diff --git a/CHANGELOG.md b/CHANGELOG.md index e2c804345..d31a1b771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,60 @@ +## 2.38.2 – 2024-10-09 +### Fixed +- error '$index is not defined' in Settings > Accounts + [#1797](https://github.com/the-djmaze/snappymail/issues/1797) + + +## 2.38.1 – 2024-10-08 + +### Added +- Admin - Extensions search filter +- Options to unset current font family and size + [#1726](https://github.com/the-djmaze/snappymail/issues/1726) +- Option to allow insecure cryptkey + [#1746](https://github.com/the-djmaze/snappymail/issues/1746) +- Save vCard FN property + [#1761](https://github.com/the-djmaze/snappymail/issues/1761) +- Docker Nginx should listen on IPv6 addresses in addition to IPv4 + [#1770](https://github.com/the-djmaze/snappymail/issues/1770) +- Full-screen Message View on Double-Click + [#1787](https://github.com/the-djmaze/snappymail/issues/1787) + +### Changed +- Use the custom Squire as submodule vendors/squire2 +- Disallow noembed and noframes HTML elements +- Keep 1 space between HTML elements in compressed templates +- Squire: sanitizeToDOMFragment now uses cleanHTML() +- Squire: improved handling of BR elements + [#1389](https://github.com/the-djmaze/snappymail/issues/1389) +- Nextcloud border-radius by @gnilebein + [#1790](https://github.com/the-djmaze/snappymail/pull/1790) +- Compose window sentFolder handling for + [#1793](https://github.com/the-djmaze/snappymail/pull/1793) +- Update Portuguese by @ner00 +- Update Polish by @tinola +- Update French by @hguilbert +- Update Portuguese (Brazil) by @mstolf + +### Fixed +- Composer dialog scroll got broken in v2.28 +- Composer dialog "from" triangle button wrong position due to font changes +- Admin - Config `search` should be ko.observable() not ko.observableArray() +- Squire: paste images + [#1389](https://github.com/the-djmaze/snappymail/issues/1389) +- Domain whitelist failures on login + [#1706](https://github.com/the-djmaze/snappymail/issues/1706) +- Sieve parse errors +- Sieve support `index` was not optional for + [#1709](https://github.com/the-djmaze/snappymail/issues/1709) +- Pagination problem for large mailbox after 2.36.1 + [#1716](https://github.com/the-djmaze/snappymail/issues/1716) +- Undefined constant "LOG_ERROR" + [#1754](https://github.com/the-djmaze/snappymail/issues/1754) +- Search Filter Capital "B" not working + [#1780](https://github.com/the-djmaze/snappymail/issues/1780) +- PHP 8.4: Implicitly nullable parameter declarations deprecated + + ## 2.38.0 – 2024-09-16 ### Added @@ -247,7 +304,7 @@ - CSS set min-width for .attachmentParent and .flagParent to line them up - cPanel use extension login-cpanel instead of login-remote - Improved login credentials handling -- Speedup Knockout a bit +- Speedup Knockout a bit and removed `with($context)` scope - Update Belarusian by @spoooyders - Update Chinese by @mayswind - Update French by @hguilbert diff --git a/README.md b/README.md index 04e17c287..b9736d393 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ This fork of RainLoop has the following changes: * CRLF => LF line endings * Embed boot.js and boot.css into index.html * Removal of old JavaScript code (things are native these days) -* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor +* Added modified [Squire](https://github.com/the-djmaze/Squire/tree/snappymail) HTML editor as replacement for CKEditor * Updated [Sabre/VObject](https://github.com/sabre-io/vobject) * Split Admin specific JavaScript code from User code * Split Sieve specific JavaScript code from User code @@ -140,28 +140,28 @@ RainLoop 1.17 vs SnappyMail |js/* |RainLoop |Snappy | |--------------- |--------: |--------: | -|admin.js |2.170.153 | 84.203 | -|app.js |4.207.787 | 446.887 | +|admin.js |2.170.153 | 84.925 | +|app.js |4.207.787 | 447.263 | |boot.js | 868.735 | 4.343 | -|libs.js | 658.812 | 236.059 | -|sieve.js | 0 | 84.571 | +|libs.js | 658.812 | 233.728 | +|sieve.js | 0 | 91.418 | |polyfills.js | 334.608 | 0 | |serviceworker.js | 0 | 285 | -|TOTAL |8.240.095 | 856.348 | +|TOTAL |8.240.095 | 861.962 | |js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |--------------- |--------: |--------: |------: |------: |--------: |--------: | -|admin.min.js | 256.831 | 41.256 | 73.606 | 13.940 | 60.877 | 12.493 | -|app.min.js | 515.367 | 201.917 |139.456 | 68.450 |110.485 | 58.321 | +|admin.min.js | 256.831 | 41.719 | 73.606 | 14.022 | 60.877 | 12.567 | +|app.min.js | 515.367 | 202.101 |139.456 | 68.505 |110.485 | 58.481 | |boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 | -|libs.min.js | 584.772 | 111.742 |180.901 | 39.882 |155.182 | 35.584 | -|sieve.min.js | 0 | 41.288 | 0 | 10.327 | 0 | 9.318 | +|libs.min.js | 584.772 | 110.646 |180.901 | 39.518 |155.182 | 35.207 | +|sieve.min.js | 0 | 45.504 | 0 | 11.131 | 0 | 9.917 | |polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 | -|TOTAL user |1.217.635 | 315.890 |358.761 |109.603 |299.485 | 94.972 | -|TOTAL user+sieve |1.217.635 | 357.178 |358.761 |119.930 |299.485 |104.290 | -|TOTAL admin | 959.099 | 155.229 |292.911 | 55.093 |249.877 | 49.144 | +|TOTAL user |1.217.635 | 314.978 |358.761 |109.294 |299.485 | 94.755 | +|TOTAL user+sieve |1.217.635 | 360.482 |358.761 |120.425 |299.485 |104.672 | +|TOTAL admin | 959.099 | 154.596 |292.911 | 54.811 |249.877 | 48.841 | -For a user it is around 67% smaller and faster than traditional RainLoop. +For a user it is around 66% smaller and faster than traditional RainLoop. ### CSS changes @@ -188,12 +188,12 @@ For a user it is around 67% smaller and faster than traditional RainLoop. |css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli | |------------ |-------: |------: |------: |------: |--------: | -|app.css | 340.331 | 84.850 | 46.946 | 17.710 | 15.161 | -|app.min.css | 274.947 | 68.192 | 39.647 | 15.602 | 13.626 | +|app.css | 340.331 | 85.073 | 46.946 | 17.792 | 15.210 | +|app.min.css | 274.947 | 68.272 | 39.647 | 15.615 | 13.636 | |boot.css | | 1.326 | | 664 | 545 | |boot.min.css | | 1.071 | | 590 | 474 | -|admin.css | | 30.761 | | 7.038 | 6.118 | -|admin.min.css | | 24.857 | | 6.360 | 5.598 | +|admin.css | | 30.880 | | 7.045 | 6.127 | +|admin.min.css | | 24.959 | | 6.368 | 5.615 | ### PGP RainLoop uses the old OpenPGP.js v2 diff --git a/dev/Common/Html.js b/dev/Common/Html.js index 23df1e025..0be300ec9 100644 --- a/dev/Common/Html.js +++ b/dev/Common/Html.js @@ -23,7 +23,7 @@ const // Structural Elements: 'blockquote','br','div','figcaption','figure','h1','h2','h3','h4','h5','h6','hgroup','hr','p','wbr', 'article','aside','header','footer','main','section', - 'details','summary', + 'details','summary','nav', // List Elements 'dd','dl','dt','li','ol','ul', // Text Formatting Elements @@ -239,6 +239,8 @@ export const linkedData: [] }, + isMsg = !!msgId, + findAttachmentByCid = cId => oAttachments.findByCid(cId), findLocationByCid = cId => { const attachment = findAttachmentByCid(cId); @@ -282,6 +284,8 @@ export const 'abbr', 'scope', // td 'colspan', 'rowspan', 'headers' + // others + //'class', 'id', 'target' ]; if (SettingsUserStore.allowStyles()) { @@ -332,7 +336,7 @@ export const tmpl.content.querySelectorAll(keepTagContent).forEach(oElement => replaceWithChildren(oElement)); tmpl.content.querySelectorAll( - ':not('+allowedTags+')' + ':not('+allowedTags+'),a:empty,span:empty' + (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '') ).forEach(oElement => oElement.remove()); /* // Is this slower or faster? @@ -481,71 +485,73 @@ export const */ let skipStyle = false; - value = delAttribute('src'); - if (value) { - if ('IMG' === name) { - oElement.loading = 'lazy'; - let attachment; - if (value.startsWith('cid:')) - { - value = value.slice(4); - setAttribute('data-x-src-cid', value); - attachment = findAttachmentByCid(value); - if (attachment?.download) { - oElement.src = attachment.linkPreview(); - oElement.title += ' ('+attachment.fileName+')'; - attachment.isInline(true); - attachment.isLinked(true); + if (isMsg) { + value = isMsg && delAttribute('src'); + if (value) { + if ('IMG' === name) { + oElement.loading = 'lazy'; + let attachment; + if (value.startsWith('cid:')) + { + value = value.slice(4); + setAttribute('data-x-src-cid', value); + attachment = findAttachmentByCid(value); + if (attachment?.download) { + oElement.src = attachment.linkPreview(); + oElement.title += ' ('+attachment.fileName+')'; + attachment.isInline(true); + attachment.isLinked(true); + } } - } - else if ((attachment = findLocationByCid(value))) - { - if (attachment.download) { - oElement.src = attachment.linkPreview(); - attachment.isLinked(true); + else if ((attachment = findLocationByCid(value))) + { + if (attachment.download) { + oElement.src = attachment.linkPreview(); + attachment.isLinked(true); + } } - } - else if (detectHiddenImages - && ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in' - || (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in' - || (oStyle.width && 2 > pInt(oStyle.width)) - || [ - 'email.microsoftemail.com/open', - 'github.com/notifications/beacon/', - '/track/open', // mandrillapp.com list-manage.com - 'google-analytics.com' - ].filter(uri => value.toLowerCase().includes(uri)).length - )) { - skipStyle = true; - oStyle.display = 'none'; -// setAttribute('style', 'display:none'); - setAttribute('data-x-src-hidden', value); -// result.tracking = true; - } - else if (httpre.test(value)) - { - let src = stripTracking(value); - if (src != value) { - result.tracking = true; - setAttribute('data-x-src-tracking', value); + else if (detectHiddenImages + && ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in' + || (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in' + || (oStyle.width && 2 > pInt(oStyle.width)) + || [ + 'email.microsoftemail.com/open', + 'github.com/notifications/beacon/', + '/track/open', // mandrillapp.com list-manage.com + 'google-analytics.com' + ].filter(uri => value.toLowerCase().includes(uri)).length + )) { + skipStyle = true; + oStyle.display = 'none'; + // setAttribute('style', 'display:none'); + setAttribute('data-x-src-hidden', value); + // result.tracking = true; + } + else if (httpre.test(value)) + { + let src = stripTracking(value); + if (src != value) { + result.tracking = true; + setAttribute('data-x-src-tracking', value); + } + setAttribute('data-x-src', src); + result.hasExternals = true; + oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20)); + } + else if (value.startsWith('data:image/')) + { + oElement.src = value; + } + else + { + setAttribute('data-x-src-broken', value); } - setAttribute('data-x-src', src); - result.hasExternals = true; - oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20)); - } - else if (value.startsWith('data:image/')) - { - oElement.src = value; } else { setAttribute('data-x-src-broken', value); } } - else - { - setAttribute('data-x-src-broken', value); - } } if (hasAttribute('background')) { @@ -631,7 +637,7 @@ export const } }); - blockquoteSwitcher(); + isMsg && blockquoteSwitcher(); // return tmpl.content.firstChild; result.html = tmpl.innerHTML.trim(); @@ -819,6 +825,7 @@ export const }; rl.Utils = { + cleanHtml: cleanHtml, htmlToPlain: htmlToPlain, plainToHtml: plainToHtml, htmlToMarkdown: htmlToMarkdown diff --git a/dev/External/SquireUI.js b/dev/External/SquireUI.js index af2bdff42..b1e50fcad 100644 --- a/dev/External/SquireUI.js +++ b/dev/External/SquireUI.js @@ -3,10 +3,6 @@ (doc => { const - removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE', - allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,FONT,H1,H2,H3,H4,H5,H6,HR,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL', - allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','), - i18n = (str, def) => rl.i18n(str) || def, ctrlKey = shortcuts.getMetaKey() + ' + ', @@ -21,38 +17,17 @@ const forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn), - getFragmentOfChildren = parent => { - let frag = doc.createDocumentFragment(); - frag.append(...parent.childNodes); - return frag; - }, - SquireDefaultConfig = { /* addLinks: true // allow_smart_html_links */ sanitizeToDOMFragment: (html, isPaste/*, squire*/) => { - tpl.innerHTML = (html||'') + html = (html||'') .replace(/<\/?(BODY|HTML)[^>]*>/gi,'') .replace(//g,'') .replace(/]*>\s*<\/span>/gi,'') .trim(); - tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove()); - if (isPaste) { - tpl.querySelectorAll(removeElements).forEach(el => el.remove()); - tpl.querySelectorAll('*').forEach(el => { - if (!el.matches(allowedElements)) { - el.replaceWith(getFragmentOfChildren(el)); - } else if (el.hasAttributes()) { - [...el.attributes].forEach(attr => { - let name = attr.name.toLowerCase(); - if (!allowedAttributes.includes(name)) { - el.removeAttribute(name); - } - }); - } - }); - } + tpl.innerHTML = isPaste ? rl.Utils.cleanHtml(html).html : html; return tpl.content; } }; @@ -411,6 +386,44 @@ class SquireUI changes.redo.input.disabled = !e.detail.canRedo; }); + squire.addEventListener('pasteImage', e => { + const items = e.detail.clipboardData.items; + let l = items.length; + while (l--) { + const item = items[l]; + if (/^image\/(png|jpeg|webp)/.test(item.type)) { + let reader = new FileReader(); + reader.onload = event => { + let img = createElement("img"), + canvas = createElement("canvas"), + ctx = canvas.getContext('2d'); + img.onload = ()=>{ + ctx.drawImage(img, 0, 0); + let width = img.width, height = img.height; + if (width > height) { + // Landscape + if (width > 1024) { + height = height * 1024 / width; + width = 1024; + } + } else if (height > 1024) { + // Portrait + width = width * 1024 / height; + height = 1024; + } + canvas.width = width; + canvas.height = height; + ctx.drawImage(img, 0, 0, width, height); + squire.insertHTML('', true); + }; + img.src = event.target.result; + } + reader.readAsDataURL(item.getAsFile()); + break; + } + } + }); + actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex; // squire.addEventListener('focus', () => shortcuts.off()); diff --git a/dev/Settings/User/General.js b/dev/Settings/User/General.js index a05a3227d..f06349c25 100644 --- a/dev/Settings/User/General.js +++ b/dev/Settings/User/General.js @@ -44,6 +44,51 @@ export class UserSettingsGeneral extends AbstractViewSettings { this.desktopNotifications = NotificationUserStore.enabled; this.isDesktopNotificationAllowed = NotificationUserStore.allowed; + const i18nSort = s => i18n('MESSAGE_LIST/SORT_'+s); + this.sortSupported = FolderUserStore.hasCapability('SORT'); + this.sortOptions = [ + { + value: '', + text: i18nSort('DATE_DESC') + }, + { + value: 'DATE', + text: i18nSort('DATE_ASC') + }, + { + value: 'FROM', + text: i18nSort('FROM_ASC') + }, + { + value: 'REVERSE FROM', + text: i18nSort('FROM_DESC') + }, + { + value: 'REVERSE SIZE', + text: i18nSort('SIZE_DESC') + }, + { + value: 'SIZE', + text: i18nSort('SIZE_ASC') + }, + { + value: 'SUBJECT', + text: i18nSort('SUBJECT_ASC') + }, + { + value: 'REVERSE SUBJECT', + text: i18nSort('SUBJECT_DESC') + }, + { + value: 'REVERSE ARRIVAL', + text: i18nSort('ARRIVAL_DESC') + }, + { + value: 'ARRIVAL', + text: i18nSort('ARRIVAL_ASC') + } + ]; + this.threadsAllowed = AppUserStore.threadsAllowed; // 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT' this.threadAlgorithms = ko.observableArray(); @@ -51,9 +96,8 @@ export class UserSettingsGeneral extends AbstractViewSettings { capa.startsWith('THREAD=') && this.threadAlgorithms.push(capa.slice(7)) ); this.threadAlgorithms.sort((a, b) => a.length - b.length); - this.threadAlgorithm = SettingsUserStore.threadAlgorithm; - ['useThreads', 'threadAlgorithm', + ['defaultSort', 'useThreads', 'threadAlgorithm', // These use addSetting() 'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval', 'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel', @@ -117,6 +161,7 @@ export class UserSettingsGeneral extends AbstractViewSettings { this.addSetting('CheckMailInterval'); this.addSetting('Layout'); this.addSetting('MaxBlockquotesLevel'); + this.addSetting('defaultSort'); this.addSettings([ 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', diff --git a/dev/Sieve/View/Filter.js b/dev/Sieve/View/Filter.js index ee7799d75..41bf85998 100644 --- a/dev/Sieve/View/Filter.js +++ b/dev/Sieve/View/Filter.js @@ -66,7 +66,7 @@ export class FilterPopupView extends rl.pluginPopupView { }); this.defaultOptionsAfterRender = defaultOptionsAfterRender; - this.folderSelectList = koComputable(() => folderListOptionsBuilder()); + this.folderSelectList = koComputable(folderListOptionsBuilder); this.selectedFolderValue.subscribe(() => this.filter().actionValueError(false)); diff --git a/dev/Stores/User/Folder.js b/dev/Stores/User/Folder.js index 37007c768..12983b373 100644 --- a/dev/Stores/User/Folder.js +++ b/dev/Stores/User/Folder.js @@ -7,6 +7,7 @@ import { forEachObjectEntry } from 'Common/Utils'; import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache'; import { Settings, SettingsCapa } from 'Common/Globals'; //import Remote from 'Remote/User/Fetch'; // Circular dependency +import { SettingsUserStore } from 'Stores/User/Settings'; export const @@ -59,7 +60,7 @@ FolderUserStore = new class { */ displaySpecSetting: false, - sortMode: '', + sortMode: SettingsUserStore.defaultSort(), quotaLimit: 0, quotaUsage: 0, diff --git a/dev/Stores/User/Settings.js b/dev/Stores/User/Settings.js index 382c74289..211a89288 100644 --- a/dev/Stores/User/Settings.js +++ b/dev/Stores/User/Settings.js @@ -29,6 +29,7 @@ export const SettingsUserStore = new class { showNextMessage: 0, allowDraftAutosave: 1, useThreads: 0, + defaultSort: '', threadAlgorithm: '', replySameFolder: 0, hideUnsubscribed: 0, @@ -103,6 +104,7 @@ export const SettingsUserStore = new class { 'showNextMessage', 'AllowDraftAutosave', 'useThreads', + 'defaultSort', 'threadAlgorithm', 'ReplySameFolder', 'HideUnsubscribed', diff --git a/dev/Styles/User/Compose.less b/dev/Styles/User/Compose.less index 8e26aef0e..56eae3dd1 100644 --- a/dev/Styles/User/Compose.less +++ b/dev/Styles/User/Compose.less @@ -22,12 +22,18 @@ .tabs { height: 100%; + max-height: 100%; + overflow: auto; } .tabs > label > * + * { margin-left: 0.5em; } + .tabs .tab-content { + overflow: auto; + } + .textAreaParent, .attachmentAreaParent { height: 100%; min-height: 200px; diff --git a/dev/Styles/User/SettingsAccounts.less b/dev/Styles/User/SettingsAccounts.less index 54e58c288..5b7f5c91a 100644 --- a/dev/Styles/User/SettingsAccounts.less +++ b/dev/Styles/User/SettingsAccounts.less @@ -45,4 +45,8 @@ content: ')'; } + .identities-list tr + tr .identity-default { + display: none; + } + } diff --git a/dev/Styles/User/SquireUI.less b/dev/Styles/User/SquireUI.less index 646ee4dd1..1a9c46ce3 100644 --- a/dev/Styles/User/SquireUI.less +++ b/dev/Styles/User/SquireUI.less @@ -56,6 +56,12 @@ .squire-wysiwyg { font-family: var(--fontSans); + /* Prevent collapse of empty elements so that the caret can be placed in there + *:empty::before { + content: "\200B"; + } + */ + ul { padding-left: 40px; li { diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index 581ad9f21..302017dfb 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -457,10 +457,21 @@ export class ComposePopupView extends AbstractViewPopup { this.from(IdentityUserStore()[0].formattedName()); } - sendCommand() { - const identity = this.currentIdentity(); - let sSentFolder = identity?.sentFolder?.() || FolderUserStore.sentFolder(); + sentFolder() + { + let sSentFolder = this.currentIdentity()?.sentFolder?.() || FolderUserStore.sentFolder(); + if (SettingsUserStore.replySameFolder()) { + if ( + 3 === arrayLength(this.aDraftInfo) && + this.aDraftInfo[2]?.length + ) { + sSentFolder = this.aDraftInfo[2]; + } + } + return UNUSED_OPTION_VALUE === sSentFolder ? null : sSentFolder; + } + sendCommand() { this.attachmentsInProcessError(false); this.attachmentsInErrorError(false); this.emptyToError(false); @@ -478,16 +489,8 @@ export class ComposePopupView extends AbstractViewPopup { } if (!this.emptyToError() && !this.attachmentsInErrorError() && !this.attachmentsInProcessError()) { - if (SettingsUserStore.replySameFolder()) { - if ( - 3 === arrayLength(this.aDraftInfo) && - this.aDraftInfo[2]?.length - ) { - sSentFolder = this.aDraftInfo[2]; - } - } - - if (!sSentFolder) { + const sSentFolder = this.sentFolder(); + if ('' === sSentFolder) { showScreenPopup(FolderSystemPopupView, [FolderType.Sent]); } else { const sendError = e => { @@ -507,8 +510,6 @@ export class ComposePopupView extends AbstractViewPopup { this.sendError(false); this.sending(true); - sSentFolder = UNUSED_OPTION_VALUE === sSentFolder ? '' : sSentFolder; - const sendMessage = params => { Remote.request('SendMessage', (iError, data) => { @@ -538,7 +539,7 @@ export class ComposePopupView extends AbstractViewPopup { this.sendError(true); sendFailed(iError, data); // Remove remembered passphrase as it could be wrong - let key = ('S/MIME' === params.sign) ? identity : null; + let key = ('S/MIME' === params.sign) ? this.currentIdentity() : null; params.signFingerprint && this.signOptions.forEach(option => ('GnuPG' === option[0]) && (key = option[1])); key && Passphrases.delete(key); @@ -560,10 +561,9 @@ export class ComposePopupView extends AbstractViewPopup { this.close(); } setFolderETag(this.draftsFolder(), ''); - setFolderETag(sSentFolder, ''); + setFolderETag(params.saveFolder, ''); if (3 === arrayLength(this.aDraftInfo)) { - const folder = this.aDraftInfo[2]; - setFolderETag(folder, ''); + setFolderETag(this.aDraftInfo[2], ''); } reloadDraftFolder(); }, diff --git a/dev/View/User/MailBox/MessageList.js b/dev/View/User/MailBox/MessageList.js index 4329dfb46..2517fe903 100644 --- a/dev/View/User/MailBox/MessageList.js +++ b/dev/View/User/MailBox/MessageList.js @@ -213,15 +213,19 @@ export class MailMessageList extends AbstractViewRight { sortText: () => { let mode = FolderUserStore.sortMode(), - desc = '' === mode || mode.includes('REVERSE'); + has = w => mode.includes(w), + desc = '' === mode || has('REVERSE'); mode = mode.split(/\s+/); - if (mode.includes('FROM')) { + if (has('FROM')) { return '@' + (desc ? '⬆' : '⬇'); } - if (mode.includes('SUBJECT')) { + if (has('SUBJECT')) { return '𝐒' + (desc ? '⬆' : '⬇'); } - return (mode.includes('SIZE') ? '✉' : '📅') + (desc ? '⬇' : '⬆'); + if (has('SIZE')) { + return '✉' + (desc ? '⬇' : '⬆'); + } + return (has('ARRIVAL') ? '📨' : '📅') + (desc ? '⬇' : '⬆'); }, downloadAsZipAllowed: () => this.attachmentsActions.includes('zip') @@ -629,8 +633,10 @@ export class MailMessageList extends AbstractViewRight { } }, dblclick: event => { - let el = eqs(event, '.messageListItem'); - el && this.gotoThread(ko.dataFor(el)); + let msg = ko.dataFor(eqs(event, '.messageListItem')); + if (msg) { + msg.threadsLen() ? this.gotoThread(msg) : toggleFullscreen(); + } } }); diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js index 3dcf461fc..5a879466e 100644 --- a/dev/View/User/MailBox/MessageView.js +++ b/dev/View/User/MailBox/MessageView.js @@ -20,7 +20,8 @@ import { fireEvent, stopEvent, addShortcut, - registerShortcut + registerShortcut, + formFieldFocused } from 'Common/Globals'; import { arrayLength } from 'Common/Utils'; @@ -467,8 +468,10 @@ export class MailMessageView extends AbstractViewRight { }); addShortcut('b', 'shift', [ScopeMessageList, ScopeMessageView], () => { - currentMessage()?.swapColors?.(); - return false; + if (!formFieldFocused()) { + currentMessage()?.swapColors?.(); + return false; + } }); addShortcut('arrowup,arrowleft', 'meta', [ScopeMessageList, ScopeMessageView], () => { diff --git a/integrations/cloudron/DESCRIPTION.md b/integrations/cloudron/DESCRIPTION.md index 599a4c1cc..c3a50a8fa 100644 --- a/integrations/cloudron/DESCRIPTION.md +++ b/integrations/cloudron/DESCRIPTION.md @@ -1,4 +1,4 @@ -This app packages SnappyMail 2.38.0. +This app packages SnappyMail 2.38.2. SnappyMail is a simple, modern, lightweight & fast web-based email client. diff --git a/integrations/cloudron/Dockerfile b/integrations/cloudron/Dockerfile index c95b06a13..a3d442e96 100644 --- a/integrations/cloudron/Dockerfile +++ b/integrations/cloudron/Dockerfile @@ -4,7 +4,7 @@ RUN mkdir -p /app/code WORKDIR /app/code # If you change the extraction below, be sure to test on scaleway -VERSION=2.38.0 +VERSION=2.38.2 RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \ unzip /tmp/snappymail.zip -d /app/code && \ rm /tmp/snappymail.zip && \ diff --git a/integrations/nextcloud/snappymail/appinfo/info.xml b/integrations/nextcloud/snappymail/appinfo/info.xml index dd1346e87..1ebad7ac1 100644 --- a/integrations/nextcloud/snappymail/appinfo/info.xml +++ b/integrations/nextcloud/snappymail/appinfo/info.xml @@ -3,7 +3,7 @@ snappymail SnappyMail SnappyMail Webmail - 2.38.0 + 2.38.2 agpl SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli { const vm = e.detail; if ('PopupsCompose' === vm.viewModelTemplateID && rl.settings.get('editorWysiwyg') === 'CompactComposer') { - vm.querySelector('.tabs label[for="tab-body"]').dataset.bind = "visible: canMailvelope"; + // add visible binding to the label + const bodyLabel = vm.querySelector('.tabs label[for="tab-body"]'); + bodyLabel.dataset.bind = 'visible: canMailvelope'; + // re-apply the binding + const labelNext = bodyLabel.nextElementSibling; + ko.removeNode(bodyLabel); + labelNext.parentElement.insertBefore(bodyLabel, labelNext); + ko.applyBindingAccessorsToNode(bodyLabel, null, vm); + // Now move the attachments tab to the bottom of the screen - const - input = vm.querySelector('.tabs input[value="attachments"]'), - label = vm.querySelector('.tabs label[for="tab-attachments"]'), - area = vm.querySelector('.tabs .attachmentAreaParent'); - input.remove(); - label.remove(); - area.remove(); + const area = vm.querySelector('.tabs .attachmentAreaParent'); + vm.querySelector('.tabs input[value="attachments"]').remove(); + vm.querySelector('.tabs label[for="tab-attachments"]').remove(); + area.querySelector('.no-attachments-desc').remove(); area.classList.add('compact'); - area.querySelector('.b-attachment-place').dataset.bind = "visible: addAttachmentEnabled(), css: {dragAndDropOver: dragAndDropVisible}"; vm.viewModelDom.append(area); + // Add and re-apply the bindings for the attachment-place + const place = area.querySelector('.b-attachment-place'); + ko.removeNode(place); + area.insertBefore(place, area.firstElementChild); + place.dataset.bind = 'visible: addAttachmentEnabled(), css: {dragAndDropOver: dragAndDropVisible}'; + ko.applyBindingAccessorsToNode(place, null, vm); // There is a better way to do this probably, // but we need this for drag and drop to work e.detail.attachmentsArea = e.detail.bodyArea; @@ -36,10 +46,6 @@ }); const - removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE', - allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,EM,FONT,H1,H2,H3,H4,H5,H6,HR,I,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL', - allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','), - // TODO: labels translations i18n = (str, def) => rl.i18n(str) || def, @@ -95,21 +101,7 @@ }, pasteSanitizer = (event) => { - const frag = event.detail.fragment; - frag.querySelectorAll('a:empty,span:empty').forEach(el => el.remove()); - frag.querySelectorAll(removeElements).forEach(el => el.remove()); - frag.querySelectorAll('*').forEach(el => { - if (!el.matches(allowedElements)) { - el.replaceWith(getFragmentOfChildren(el)); - } else if (el.hasAttributes()) { - [...el.attributes].forEach(attr => { - let name = attr.name.toLowerCase(); - if (!allowedAttributes.includes(name)) { - el.removeAttribute(name); - } - }); - } - }); + return rl.Utils.cleanHtml(event.detail.html).html; }, pasteImageHandler = (e, squire) => { @@ -317,7 +309,8 @@ clr.style.left = (input.offsetLeft + input.parentNode.offsetLeft) + 'px'; clr.style.width = input.offsetWidth + 'px'; - clr.value = ''; + // firefox does not call "onchange" for #000 if we use clr.value='' + clr.value = '#00ff0c'; clr.onchange = () => { switch (name) { case 'color': diff --git a/plugins/custom-login-mapping/index.php b/plugins/custom-login-mapping/index.php index f282b0f60..e7d287b5f 100644 --- a/plugins/custom-login-mapping/index.php +++ b/plugins/custom-login-mapping/index.php @@ -12,6 +12,7 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin public function Init() : void { + // Happens only at DoLogin | ServiceSso | DoAccountSetup $this->addHook('login.credentials', 'FilterLoginCredentials'); } @@ -29,18 +30,14 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin if (!empty($sMapping)) { $aLines = \explode("\n", \preg_replace('/[\r\n\t\s]+/', "\n", $sMapping)); foreach ($aLines as $sLine) { - if (false !== \strpos($sLine, ':')) { - $aData = \explode(':', $sLine, 3); - if (\is_array($aData) && !empty($aData[0]) && isset($aData[1])) { - $aData = \array_map('trim', $aData); - if ($sEmail === $aData[0]) { - if (\strlen($aData[1])) { - $sImapUser = $aData[1]; - } - if (isset($aData[2]) && \strlen($aData[2])) { - $sSmtpUser = $aData[2]; - } - } + $aData = \explode(':', $sLine, 3); + if (\is_array($aData) && isset($aData[1]) && \trim($aData[0]) === $sEmail) { + $aData = \array_map('trim', $aData); + if (\strlen($aData[1])) { + $sImapUser = $aData[1]; + } + if (isset($aData[2]) && \strlen($aData[2])) { + $sSmtpUser = $aData[2]; } } } diff --git a/plugins/login-o365/LoginOAuth2.js b/plugins/login-o365/LoginOAuth2.js index 4afc930bf..dd8c843ed 100644 --- a/plugins/login-o365/LoginOAuth2.js +++ b/plugins/login-o365/LoginOAuth2.js @@ -1,11 +1,13 @@ (rl => { const client_id = rl.pluginSettingsGet('login-o365', 'client_id'), + // https://learn.microsoft.com/en-us/entra/identity-platform/reply-url#query-parameter-support-in-redirect-uris + query = rl.pluginSettingsGet('login-o365', 'personal') ? '' : '?', tenant = rl.pluginSettingsGet('login-o365', 'tenant'), login = () => { document.location = 'https://login.microsoftonline.com/'+tenant+'/oauth2/v2.0/authorize?' + (new URLSearchParams({ response_type: 'code', client_id: client_id, - redirect_uri: document.location.href.replace(/\/$/, '') + '/LoginO365', + redirect_uri: document.location.href.replace(/\/$/, '') + '/' + query + 'LoginO365', scope: [ // Associate personal info 'openid', diff --git a/plugins/login-o365/index.php b/plugins/login-o365/index.php index 902c46d15..62050f429 100644 --- a/plugins/login-o365/index.php +++ b/plugins/login-o365/index.php @@ -7,8 +7,9 @@ * * https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps * - * redirect_uri=https://{DOMAIN}/?LoginO365 - * redirect_uri=https://{DOMAIN}/LoginO365 + * https://learn.microsoft.com/en-us/entra/identity-platform/reply-url#query-parameter-support-in-redirect-uris + * Azure: redirect_uri=https://{DOMAIN}/?LoginO365 + * Personal: redirect_uri=https://{DOMAIN}/LoginO365 */ use RainLoop\Model\MainAccount; @@ -18,8 +19,8 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin { const NAME = 'Office365/Outlook OAuth2', - VERSION = '0.2', - RELEASE = '2024-08-13', + VERSION = '0.3', + RELEASE = '2024-09-29', REQUIRED = '2.36.1', CATEGORY = 'Login', DESCRIPTION = 'Office365/Outlook IMAP, Sieve & SMTP login using RFC 7628 OAuth2'; @@ -47,6 +48,7 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin public function httpPaths(array &$aPaths) : void { + // Personal accounts workaround if (!empty($_SERVER['PATH_INFO']) && \str_ends_with($_SERVER['PATH_INFO'], 'LoginO365')) { $aPaths = ['LoginO365']; } @@ -113,7 +115,7 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin $iExpires += $aResponse['expires_in']; $oO365->setAccessToken($sAccessToken); - $aUserInfo = $oO365->fetch('https://graph.microsoft.com/oidc/userinfo"'); + $aUserInfo = $oO365->fetch('https://graph.microsoft.com/oidc/userinfo'); if (200 != $aUserInfo['code']) { throw new \RuntimeException("HTTP: {$aResponse['code']}"); } @@ -154,6 +156,12 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin public function configMapping() : array { return [ + \RainLoop\Plugins\Property::NewInstance('personal') + ->SetLabel('Use with personal accounts') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(true) + ->SetAllowedInJs() + ->SetDescription('Sign in users with personal Microsoft accounts such as Outlook.com (Hotmail)'), \RainLoop\Plugins\Property::NewInstance('client_id') ->SetLabel('Client ID') ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING) diff --git a/plugins/nextcloud/index.php b/plugins/nextcloud/index.php index 734d86825..482a5d7aa 100644 --- a/plugins/nextcloud/index.php +++ b/plugins/nextcloud/index.php @@ -4,8 +4,8 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin { const NAME = 'Nextcloud', - VERSION = '2.38.0', - RELEASE = '2024-09-16', + VERSION = '2.38.1', + RELEASE = '2024-10-08', CATEGORY = 'Integrations', DESCRIPTION = 'Integrate with Nextcloud v20+', REQUIRED = '2.38.0'; @@ -55,7 +55,6 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin $this->addTemplate('templates/PopupsNextcloudFiles.html'); $this->addTemplate('templates/PopupsNextcloudCalendars.html'); - $this->addTemplate('templates/PopupsNextcloudInvites.html'); // $this->addHook('login.credentials.step-2', 'loginCredentials2'); // $this->addHook('login.credentials', 'loginCredentials'); diff --git a/plugins/nextcloud/js/message.js b/plugins/nextcloud/js/message.js index bab6793a4..f5154b745 100644 --- a/plugins/nextcloud/js/message.js +++ b/plugins/nextcloud/js/message.js @@ -21,18 +21,9 @@ // https://github.com/nextcloud/calendar/issues/4684 if (cfg.CalDAV) { - attachmentsControls.append(Element.fromHTML(` + attachmentsControls.append(Element.fromHTML(` `)); - attachmentsControls.append(Element.fromHTML(` - - `)) - attachmentsControls.append(Element.fromHTML(` - - `)) - attachmentsControls.append(Element.fromHTML(` - - `)) } } /* @@ -116,169 +107,18 @@ }; view.nextcloudICS = ko.observable(null); - view.nextcloudICSOldInvitation = ko.observable(null); - view.nextcloudICSNewInvitation = ko.observable(null); - view.nextcloudICSLastInvitation = ko.observable(null); - view.nextcloudICSShow = ko.observable(null); - - view.nextcloudICSCalendar = ko.observable(null); - view.filteredEventsUrls = ko.observable([]); - - view.nextcloudSaveICS = async () => { + view.nextcloudSaveICS = () => { let VEVENT = view.nextcloudICS(); - VEVENT = await view.handleUpdatedRecurrentEvents(VEVENT) - VEVENT && rl.nextcloud.selectCalendar(VEVENT) + VEVENT && rl.nextcloud.selectCalendar() + .then(href => href && rl.nextcloud.calendarPut(href, VEVENT)); } - - view.handleUpdatedRecurrentEvents = async (VEVENT) => { - - const uid = VEVENT.UID - - let makePUTRequest = false - - const filteredEventUrls = view.filteredEventUrls - - if (filteredEventUrls.length > 1) { - // console.warn('filteredEventUrls.length > 1') - } - else if (filteredEventUrls.length == 1) { - const eventUrl = filteredEventUrls[0] - const eventText = await fetchEvent(eventUrl) - - // don't do anything for equal cards - if (VEVENT.rawText == eventText) { - return VEVENT - } - - const newVeventsText = extractVEVENTs(VEVENT.rawText) - const oldVeventsText = extractVEVENTs(eventText) - - // if there's only one event in the old card, it can't be an edit of the exception card - if (oldVeventsText.length == 1) { - const newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventsText[0]) - - // if there's a property RECURRENCE-ID in the new card, it's an recurrence exception event - if (newRecurrenceId.length > 0) { - const updatedVEVENT = { - 'SUMMARY' : VEVENT.SUMMARY, - 'UID' : VEVENT.UID, - 'rawText' : mergeEventTexts(eventText, newVeventsText[0]) - } - VEVENT = updatedVEVENT - makePUTRequest = true - } - else { - return VEVENT - } - } - // if there's more than one event in the old card, it's possible to be an inclusion of a new exception card - // or update of old exception card - else { - let recurrenceIdMatch = false - - let isUpdate = false - const updateData = { - 'oldEventIndex': null, - 'newEventIndex': null, - } - - // check if it's an update - for (let i = 0; i < oldVeventsText.length; i++) { - let oldVeventText = oldVeventsText[i] - - for (let j = 0; j < newVeventsText.length; j++) { - let newVeventText = newVeventsText[j] - - let oldRecurrenceId = extractProperties('RECURRENCE-ID', oldVeventText) - let newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventText) - let oldSequence = extractProperties('SEQUENCE', oldVeventText) - let newSequence = extractProperties('SEQUENCE', newVeventText) - - if (oldRecurrenceId.length == 0 || newRecurrenceId.length == 0 || oldSequence.length == 0 || newSequence.length == 0) { - continue - } - - if (oldRecurrenceId[0] == newRecurrenceId[0]) { - if (newSequence[0] > oldSequence[0]) { - isUpdate = true - - updateData.oldEventIndex = i - updateData.newEventIndex = j - - i = oldVeventsText.length - j = newVeventsText.length - } - } - } - } - - // if it's an update... - if (isUpdate) { - // substitute old event text for new event text - const oldEventStart = eventText.indexOf(oldVeventsText[updateData.oldEventIndex]) - const oldEventEnd = oldEventStart + oldVeventsText[updateData.oldEventIndex].length - - const newEvent = eventText.substring(0, oldEventStart) + newVeventsText[updateData.newEventIndex] + eventText.substring(oldEventEnd) - - const updatedVEVENT = { - 'SUMMARY' : VEVENT.SUMMARY, - 'UID': VEVENT.UID, - 'rawText' : newEvent - } - VEVENT = updatedVEVENT - makePUTRequest = true - } - // if it's not an update, it's an inclusion, as there's no match of RECURRENCE-ID - else { - const updatedVEVENT = { - 'SUMMARY' : VEVENT.SUMMARY, - 'UID' : VEVENT.UID, - 'rawText' : mergeEventTexts(eventText, newVeventsText[0]) - } - VEVENT = updatedVEVENT - makePUTRequest = true - } - } - } - - if (makePUTRequest) { - let href = "/" + (filteredEventUrls[0].split('/').slice(5, -1)[0]) - - rl.nextcloud.calendarPut(href, VEVENT, (response) => { - if (response.status != 201 && response.status != 204) { - InvitesPopupView.showModal([ - rl.i18n('NEXTCLOUD/EVENT_UPDATE_FAILURE_TITLE'), - rl.i18n('NEXTCLOUD/EVENT_UPDATE_FAILURE_BODY', {eventName: VEVENT.SUMMARY}) - ]) - return - } - InvitesPopupView.showModal([ - rl.i18n('NEXTCLOUD/EVENT_UPDATED_TITLE'), - rl.i18n('NEXTCLOUD/EVENT_UPDATED_BODY', {eventName: VEVENT.SUMMARY}) - ]) - }) - - return null - } - - return VEVENT - } - - - /** * TODO */ view.message.subscribe(msg => { view.nextcloudICS(null); - view.nextcloudICSOldInvitation(null); - view.nextcloudICSNewInvitation(null); - view.nextcloudICSLastInvitation(null); - view.nextcloudICSShow(view.nextcloudICS()) - - if (msg && cfg.CalDAV) { // let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType); let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType); @@ -286,7 +126,7 @@ // fetch it and parse the VEVENT rl.fetch(ics.linkDownload()) .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) - .then(async (text) => { + .then(text => { let VEVENT, VALARM, multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE', @@ -337,158 +177,6 @@ shouldReply: VEVENT.shouldReply() }); view.nextcloudICS(VEVENT); - view.nextcloudICSShow(true); - - - // try to get calendars, save - const calendarUrls = await fetchCalendarUrls() - - const filteredEventUrls = [] - for (let i = 0; i < calendarUrls.length; i++) { - let calendarUrl = calendarUrls[i] - - const skipCalendars = ['/inbox/', '/outbox/', '/trashbin/'] - let skip = false - for (let j = 0; j < skipCalendars.length; j++) { - if (calendarUrl.includes(skipCalendars[j])) { - skip = true - break - } - } - if (skip) { - continue - } - - // try to get event - const eventUrls = await fetchEventUrl(calendarUrl, VEVENT.UID) - - if (eventUrls.length == 0) { - continue - } - - eventUrls.forEach((url) => { - filteredEventUrls.push(url) - }) - } - view.filteredEventUrls = filteredEventUrls - - // if there's none, save in view.nextcloudICS - if (filteredEventUrls.length == 0) { - view.nextcloudICS(VEVENT); - view.nextcloudICSShow(true) - } - // if there's some... - else { - const savedEvent = await fetchEvent(filteredEventUrls[0]) - - const newVeventsText = extractVEVENTs(VEVENT.rawText) - const oldVeventsText = extractVEVENTs(savedEvent) - - // if there's more than one event in the old card, it's possible to be inclusion of new exception card - // or updated of old exception card - if (oldVeventsText.length > 1) { - let recurrenceIdMatch = false - - let oldNewestCreated = null - let newNewestCreated = null - - // check if it's an update - for (let i = 0; i < oldVeventsText.length; i++) { - let oldVeventText = oldVeventsText[i] - - for (let j = 0; j < newVeventsText.length; j++) { - let newVeventText = newVeventsText[j] - - let oldRecurrenceId = extractProperties('RECURRENCE-ID', oldVeventText) - let newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventText) - let oldSequence = extractProperties('SEQUENCE', oldVeventText) - let newSequence = extractProperties('SEQUENCE', newVeventText) - - if (newRecurrenceId.length == 0 && oldRecurrenceId.length == 1) { - view.nextcloudICSShow(false) - view.nextcloudICSOldInvitation(true) - } - - if (oldRecurrenceId.length > 0 && newRecurrenceId.length > 0 && oldSequence.length > 0 && newSequence.length > 0) { - if (oldRecurrenceId[0] == newRecurrenceId[0]) { - if (newSequence[0] < oldSequence[0]) { - view.nextcloudICSOldInvitation(true) - view.nextcloudICSShow(false) - } - else if (newSequence[0] == oldSequence[0]) { - view.nextcloudICSLastInvitation(true) - view.nextcloudICSShow(false) - } - else if (newSequence[0] > oldSequence[0]) { - view.nextcloudICSNewInvitation(true) - view.nextcloudICSShow(false) - } - - // exit for loops - j = newVeventsText.length - i = oldVeventsText.length - } - } - - let oldCreated = extractProperties('CREATED', oldVeventText) - let newCreated = extractProperties('CREATED', newVeventText) - - if (oldCreated.length == 0 || newCreated.length == 0) { - continue - } - - const formattedOldDate = oldCreated[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z'); - const formattedNewDate = newCreated[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z'); - - const oldDate = new Date(formattedOldDate) - const newDate = new Date(formattedNewDate) - - if (oldNewestCreated == null || oldDate > oldNewestCreated) { - oldNewestCreated = oldDate - } - if (newNewestCreated == null || newDate > newNewestCreated) { - newNewestCreated = newDate - } - } - } - - if (newNewestCreated != null && oldNewestCreated != null) { - if (newNewestCreated < oldNewestCreated) { - view.nextcloudICSOldInvitation(true) - view.nextcloudICSShow(false) - } - else if (newNewestCreated == oldNewestCreated) { - view.nextcloudICSLastInvitation(true) - view.nextcloudICSShow(false) - } - } - } - else { - const oldLastModified = extractProperties('LAST-MODIFIED', oldVeventsText[0]) - const newLastModified = extractProperties('LAST-MODIFIED', newVeventsText[0]) - - if (oldLastModified.length == 1 && newLastModified.length == 1) { - const formattedOldDate = oldLastModified[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z'); - const formattedNewDate = newLastModified[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z'); - - const oldDate = new Date(formattedOldDate) - const newDate = new Date(formattedNewDate) - - if (newDate > oldDate) { - view.nextcloudICSNewInvitation(true) - view.nextcloudICSShow(false) - } - else if (newDate < oldDate) { - view.nextcloudICSOldInvitation(true) - view.nextcloudICSShow(false) - } - else { - view.nextcloudICSLastInvitation(true) - view.nextcloudICSShow(false) - } - } - } - } } }); } @@ -498,148 +186,3 @@ }); })(window.rl); - - -async function fetchCalendarUrls () { - const username = OC.currentUser - const requestToken = OC.requestToken - - const url = '/remote.php/dav/calendars/' + username - const response = await fetch(url, { - 'method': 'PROPFIND', - 'headers': { - 'Depth': '1', - 'Content-Type': 'application/xml', - 'requesttoken' : requestToken - } - }) - - if (!response.ok) { - throw new Error('Error fetching calendars', response) - } - - const responseText = await response.text() - - const parser = new DOMParser() - const xmlDoc = parser.parseFromString(responseText, 'application/xml') - - const calendarUrls = [] - - const hrefElements = xmlDoc.getElementsByTagName('d:href') - for (let i = 1; i < hrefElements.length; i++) { - let calendarUrl = hrefElements[i].textContent.trim() - calendarUrls.push(calendarUrl) - } - - return calendarUrls -} - - -async function fetchEventUrl (calendarUrl, uid) { - const requestToken = OC.requestToken - - const xmlRequestBody = ` - - - - - - - - - ${uid} - - - - -` - - const response = await fetch(calendarUrl, { - 'method': 'REPORT', - 'headers': { - 'Content-Type': 'application/xml', - 'Depth': 1, - 'requesttoken': requestToken - }, - 'body': xmlRequestBody - }) - - const responseText = await response.text() - - const parser = new DOMParser() - const xmlDoc = parser.parseFromString(responseText, 'application/xml') - - const hrefElements = xmlDoc.getElementsByTagName('d:href') - const hrefValues = Array.from(hrefElements).map(element => element.textContent) - - return hrefValues -} - - -async function fetchEvent (eventUrl) { - const requestToken = OC.requestToken - const response = await fetch(eventUrl, { - 'method': 'GET', - 'headers': { - 'requesttoken': requestToken - } - }) - const responseText = await response.text() - - return responseText -} - - -function extractVEVENTs(text) { - let lastEndIndex = 0 - const vEvents = [] - - let textToSearch = "" - let beginIndex = 0 - let endIndex = 0 - let foundVevent = false - - while (true) { - textToSearch = text.substring(lastEndIndex) - - beginIndex = textToSearch.indexOf('BEGIN:VEVENT') - if (beginIndex == -1) { - break - } - endIndex = textToSearch.substring(beginIndex).indexOf('END:VEVENT') - if (endIndex == -1) { - break - } - endIndex += beginIndex - - lastEndIndex = lastEndIndex + endIndex + 'END:VEVENT'.length - - foundVevent = textToSearch.substring(beginIndex + 'BEGIN:VEVENT'.length, endIndex) - - vEvents.push(foundVevent) - } - - return vEvents -} - - -function extractProperties (property, text) { - const matches = text.match(`${property}.*`) - - if (matches == null) { - return [] - } - - const separatedMatches = matches.map((match) => { - return match.substring(property.length + 1) - }) - - return separatedMatches -} - - -function mergeEventTexts (oldEventText, newEventText) { - const appendIndex = oldEventText.indexOf('END:VEVENT') + 'END:VEVENT'.length - const updatedEventText = oldEventText.substring(0, appendIndex) + "\nBEGIN:VEVENT" + newEventText + "END:VEVENT" + oldEventText.substring(appendIndex) - return updatedEventText -} diff --git a/plugins/nextcloud/js/webdav.js b/plugins/nextcloud/js/webdav.js index 4f2e2cafd..24aa1f226 100644 --- a/plugins/nextcloud/js/webdav.js +++ b/plugins/nextcloud/js/webdav.js @@ -320,28 +320,12 @@ class NextcloudCalendarsPopupView extends rl.pluginPopupView { } onBuild(dom) { - let modalObj = this this.tree = dom.querySelector('#sm-nc-calendars'); this.tree.addEventListener('click', event => { let el = event.target; if (el.matches('button')) { this.select = el.href; - let VEVENT = this.VEVENT - - rl.nextcloud.calendarPut(this.select, this.VEVENT, (response) => { - if (response.status != 201 && response.status != 204) { - InvitesPopupView.showModal([ - rl.i18n('MESSAGE/EVENT_ADDITION_FAILURE_TITLE'), - rl.i18n('MESSAGE/EVENT_ADDITION_FAILURE_BODY', {eventName: VEVENT.SUMMARY}), - () => {modalObj.close()} - ]) - } - InvitesPopupView.showModal([ - rl.i18n('MESSAGE/EVENT_ADDED_TITLE'), - rl.i18n('MESSAGE/EVENT_ADDED_BODY', {eventName: VEVENT.SUMMARY}), - () => {modalObj.close()} - ]) - }) + this.close(); } }); } @@ -391,10 +375,9 @@ class NextcloudCalendarsPopupView extends rl.pluginPopupView { treeElement.appendChild(li); } // Happens after showModal() - beforeShow(fResolve, VEVENT) { + beforeShow(fResolve) { this.select = ''; this.fResolve = fResolve; - this.VEVENT = VEVENT this.tree.innerHTML = ''; davFetch('calendars', '/', { method: 'PROPFIND', @@ -449,15 +432,14 @@ close() {} } rl.nextcloud = { - selectCalendar: (VEVENT) => + selectCalendar: () => new Promise(resolve => { NextcloudCalendarsPopupView.showModal([ href => resolve(href), - VEVENT ]); }), - calendarPut: (path, event, callback) => { + calendarPut: (path, event) => { davFetch('calendars', path + '/' + event.UID + '.ics', { method: 'PUT', headers: { @@ -481,8 +463,6 @@ rl.nextcloud = { // response.text().then(text => console.error({status:response.status, body:text})); Promise.reject(new Error({ response })); } - - callback && callback(response) }); }, @@ -520,27 +500,3 @@ function getElementsInNamespaces(xmlDocument, tagName) { } return results; } - - -class InvitesPopupView extends rl.pluginPopupView { - constructor() { - super('NextcloudInvites') - } - - onBuild(dom) { - this.title = dom.querySelector('#sm-invites-popup-title') - this.body = dom.querySelector('#sm-invites-popup-body') - } - - beforeShow(title, body, onHide_) { - this.title.innerHTML = title - this.body.innerHTML = body - this.onHide_ = onHide_ - } - - onHide() { - if (this.onHide_) { - this.onHide_() - } - } -} diff --git a/plugins/nextcloud/langs/en.json b/plugins/nextcloud/langs/en.json index 845bf98c0..217de463d 100644 --- a/plugins/nextcloud/langs/en.json +++ b/plugins/nextcloud/langs/en.json @@ -2,21 +2,14 @@ "NEXTCLOUD": { "SAVE_ATTACHMENTS": "Save in Nextcloud", "SAVE_EML": "Save as .eml in Nextcloud", + "SAVE_ICS": "Add to calendar", "SELECT_FOLDER": "Select folder", "SELECT_FILES": "Select file(s)", "ATTACH_FILES": "Attach Nextcloud files", + "SELECT_CALENDAR": "Select calendar", "FILE_ATTACH": "attach", "FILE_INTERNAL": "internal", - "FILE_PUBLIC": "public", "ADDRESS_BOOK": "Address Book", - "SELECT_CALENDAR": "Select calendar", - "SAVE_ICS": "Add to calendar", - "OLD_INVITATION": "Old invitation", - "UPDATE_ON_MY_CALENDAR": "Update invitation", - "LAST_INVITATION": "Last invitation", - "EVENT_UPDATE_FAILURE_TITLE": "Update failed", - "EVENT_UPDATE_FAILURE_BODY": "Why, i have no clue", - "EVENT_UPDATED_TITLE": "Updated", - "EVENT_UPDATED_BODY": "Why, i have no clue" + "FILE_PUBLIC": "public" } } diff --git a/plugins/nextcloud/templates/PopupsNextcloudInvites.html b/plugins/nextcloud/templates/PopupsNextcloudInvites.html deleted file mode 100644 index 6056fb2ed..000000000 --- a/plugins/nextcloud/templates/PopupsNextcloudInvites.html +++ /dev/null @@ -1,8 +0,0 @@ -
- × -

-
- -
-
diff --git a/plugins/send-save-in/LICENSE b/plugins/send-save-in/LICENSE new file mode 100644 index 000000000..f709b02e2 --- /dev/null +++ b/plugins/send-save-in/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2016 RainLoop Team + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/send-save-in/index.php b/plugins/send-save-in/index.php new file mode 100644 index 000000000..2289c18f3 --- /dev/null +++ b/plugins/send-save-in/index.php @@ -0,0 +1,23 @@ +UseLangs(true); // start use langs folder + $this->addJs('savein.js'); + } +} diff --git a/plugins/send-save-in/savein.js b/plugins/send-save-in/savein.js new file mode 100644 index 000000000..b95e18892 --- /dev/null +++ b/plugins/send-save-in/savein.js @@ -0,0 +1,75 @@ +(rl => { + const templateId = 'PopupsCompose', + folderListOptionsBuilder = () => { + const + aResult = [{ + id: '', + name: '', + system: false, + disabled: false + }], + sDeepPrefix = '\u00A0\u00A0\u00A0', + showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/, + + foldersWalk = folders => { + folders.forEach(oItem => { + if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) { + aResult.push({ + id: oItem.fullName, + name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(), + system: false, + disabled: !oItem.selectable() + }); + } + + if (oItem.subFolders.length) { + foldersWalk(oItem.subFolders()); + } + }); + }; + + + // FolderUserStore.folderList() + foldersWalk(rl.app.folderList() || []); + + return aResult; + }; + + let oldSentFolderFn; + + addEventListener('rl-view-model.create', e => { + if (templateId === e.detail.viewModelTemplateID) { + const view = e.detail; // ComposePopupView + + view.sentFolderValue = ko.observable(''); + view.sentFolderSelectList = ko.computed(folderListOptionsBuilder, {'pure':true}); + view.defaultOptionsAfterRender = (domItem, item) => + item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled); + + oldSentFolderFn = view.sentFolder.bind(view); + view.sentFolder = () => view.sentFolderValue() || oldSentFolderFn(); + + document.getElementById(templateId).content.querySelector('.b-header tbody').append(Element.fromHTML(` + + Store in + + + (When send, store a copy of the message in the selected folder) + + `)); + + view.currentIdentity.subscribe(()=>{ + view.sentFolderValue(oldSentFolderFn()); + }); + } + }); + + addEventListener('rl-vm-visible', e => { + if (templateId === e.detail.viewModelTemplateID) { + const view = e.detail; // ComposePopupView + view.sentFolderValue(oldSentFolderFn()); + } + }); + +})(window.rl); diff --git a/plugins/two-factor-auth/langs/cs.ini b/plugins/two-factor-auth/langs/cs.ini index bcffceb9c..2324476fc 100644 --- a/plugins/two-factor-auth/langs/cs.ini +++ b/plugins/two-factor-auth/langs/cs.ini @@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "Show Secret" BUTTON_HIDE_SECRET = "Hide Secret" TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configured" TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Not configured" -TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually.\n" -TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive.\n" +TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually." +TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive." TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test." TITLE_TEST_CODE = "2-Step verification test" LABEL_CODE = "Code" diff --git a/plugins/two-factor-auth/langs/en.ini b/plugins/two-factor-auth/langs/en.ini index fb94a5e56..003a50a31 100644 --- a/plugins/two-factor-auth/langs/en.ini +++ b/plugins/two-factor-auth/langs/en.ini @@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "Show Secret" BUTTON_HIDE_SECRET = "Hide Secret" TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configured" TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Not configured" -TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually.\n" -TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive.\n" +TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually." +TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive." TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test." TITLE_TEST_CODE = "2-Step verification test" LABEL_CODE = "Code" diff --git a/plugins/two-factor-auth/langs/hu.ini b/plugins/two-factor-auth/langs/hu.ini index c97b8b192..a847b1be4 100644 --- a/plugins/two-factor-auth/langs/hu.ini +++ b/plugins/two-factor-auth/langs/hu.ini @@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "Titok megjelenítése" BUTTON_HIDE_SECRET = "Titok elrejtése" TWO_FACTOR_SECRET_CONFIGURED_DESC = "Beállítva" TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Nincs beállítva" -TWO_FACTOR_SECRET_DESC = "Importáld ezt az infót a Google Authenticator kliensedbe (vagy más TOTP kliensbe) az alábbi QR kód használatával vagy a kód manuális megadatásával.\n" -TWO_FACTOR_BACKUP_CODES_DESC = "Ha nem kapod meg a kódokat a Google Authenticator kliensből (vagy más TOTP kliensből), akkor a bejelentkezéshez használhatod a biztonsági kódot. A biztonsági kód használata után inaktívvá válik.\n" +TWO_FACTOR_SECRET_DESC = "Importáld ezt az infót a Google Authenticator kliensedbe (vagy más TOTP kliensbe) az alábbi QR kód használatával vagy a kód manuális megadatásával." +TWO_FACTOR_BACKUP_CODES_DESC = "Ha nem kapod meg a kódokat a Google Authenticator kliensből (vagy más TOTP kliensből), akkor a bejelentkezéshez használhatod a biztonsági kódot. A biztonsági kód használata után inaktívvá válik." TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Tesztelés nélkül nem lehet megváltoztatni ezt a beállítást." TITLE_TEST_CODE = "2-lépéses hitelesítés teszt" LABEL_CODE = "Kód" diff --git a/plugins/two-factor-auth/langs/it.ini b/plugins/two-factor-auth/langs/it.ini index 1b1faccac..fbd44f45d 100644 --- a/plugins/two-factor-auth/langs/it.ini +++ b/plugins/two-factor-auth/langs/it.ini @@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "Mostra la chiave segreta" BUTTON_HIDE_SECRET = "Nascondi la chiave segreta" TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configurato" TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Non configurato" -TWO_FACTOR_SECRET_DESC = "Importa queste informazioni nella tua app Google Authenticator (o un altro programma per l'autenticazione a due fattori) utilizzando il codice QR fornito qui sotto o inserendo il codice manualmente.\n" -TWO_FACTOR_BACKUP_CODES_DESC = "Se non riesci a ricevere i codici tramite Google Authenticator (o un altro programma per l'autenticazione a due fattori), puoi utilizzare i codici di backup per accedere. Dopo che hai utilizzato un codice di backup per accedere, diventerà inattivo.\n" +TWO_FACTOR_SECRET_DESC = "Importa queste informazioni nella tua app Google Authenticator (o un altro programma per l'autenticazione a due fattori) utilizzando il codice QR fornito qui sotto o inserendo il codice manualmente." +TWO_FACTOR_BACKUP_CODES_DESC = "Se non riesci a ricevere i codici tramite Google Authenticator (o un altro programma per l'autenticazione a due fattori), puoi utilizzare i codici di backup per accedere. Dopo che hai utilizzato un codice di backup per accedere, diventerà inattivo." TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Non puoi modificare questa impostazione prima del test." TITLE_TEST_CODE = "Test di verifica a 2 fattori" LABEL_CODE = "Codice" diff --git a/plugins/two-factor-auth/langs/uk.ini b/plugins/two-factor-auth/langs/uk.ini new file mode 100644 index 000000000..5f09cfb09 --- /dev/null +++ b/plugins/two-factor-auth/langs/uk.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "2-етапна перевірка (TOTP)" +LABEL_ENABLE_TWO_FACTOR = "Увімкнути 2-етапну перевірку" +LABEL_TWO_FACTOR_USER = "Ім'я користувача" +LABEL_TWO_FACTOR_STATUS = "Статус" +LABEL_TWO_FACTOR_SECRET = "Ключ" +LABEL_TWO_FACTOR_BACKUP_CODES = "Резервні коди" +BUTTON_CREATE = "Create a secret" +BUTTON_ACTIVATE = "Activate" +LINK_TEST = "Тестувати" +BUTTON_SHOW_SECRET = "Показати ключ" +BUTTON_HIDE_SECRET = "Приховати ключ" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "Налаштовано" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Не налаштовано" +TWO_FACTOR_SECRET_DESC = "Імпортуйте цю інформацію у свій клієнт Google Authenticator (або інший клієнт TOTP), використовуючи наданий нижче QR-код або ввівши код вручну" +TWO_FACTOR_BACKUP_CODES_DESC = "Якщо ви не можете отримати код через Google Authenticator (або інший клієнт TOTP), ви можете скористатися резервними кодами для входу. Після того, як ви використаєте резервний код для входу, він стане неактивним. Закреслюйте його" +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Ви не можете змінити це налаштування перед тестуванням." +TITLE_TEST_CODE = "Тест 2-етапної перевірки" +LABEL_CODE = "Код" +LABEL_TWO_FACTOR_CODE = "Код підтвердження" diff --git a/plugins/two-factor-auth/langs/zh.ini b/plugins/two-factor-auth/langs/zh.ini index 56091d914..87c530e92 100644 --- a/plugins/two-factor-auth/langs/zh.ini +++ b/plugins/two-factor-auth/langs/zh.ini @@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "显示密钥" BUTTON_HIDE_SECRET = "隐藏密钥" TWO_FACTOR_SECRET_CONFIGURED_DESC = "已设置" TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "未设置" -TWO_FACTOR_SECRET_DESC = "将下面的信息导入 Google Authenticator (或其他 TOTP 客户端)。 扫描下面的二维码或手动输入密钥。\n" -TWO_FACTOR_BACKUP_CODES_DESC = "如果你无法从验证器获取验证码,你可以使用备用代码登录。 当你使用过某一个备用代码后,它将会失效。\n" +TWO_FACTOR_SECRET_DESC = "将下面的信息导入 Google Authenticator (或其他 TOTP 客户端)。 扫描下面的二维码或手动输入密钥。" +TWO_FACTOR_BACKUP_CODES_DESC = "如果你无法从验证器获取验证码,你可以使用备用代码登录。 当你使用过某一个备用代码后,它将会失效。" TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "完成测试前你无法更改此设置。" TITLE_TEST_CODE = "两步验证测试" LABEL_CODE = "代码" diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php index da6b9e199..4e78236e9 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php @@ -69,6 +69,7 @@ trait Accounts } /** + * Add/Edit additional account * @throws \MailSo\RuntimeException */ public function DoAccountSetup(): array @@ -209,6 +210,7 @@ trait Accounts 'mainEmail' => \RainLoop\Api::Actions()->getMainAccountFromToken()->Email(), 'contactsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(), 'HideUnsubscribed' => false, + 'defaultSort' => '', 'useThreads' => (bool) $oConfig->Get('defaults', 'mail_use_threads', false), 'threadAlgorithm' => '', 'ReplySameFolder' => (bool) $oConfig->Get('defaults', 'mail_reply_same_folder', false), @@ -225,6 +227,7 @@ trait Accounts $aResult['TrashFolder'] = (string) $oSettingsLocal->GetConf('TrashFolder', ''); $aResult['ArchiveFolder'] = (string) $oSettingsLocal->GetConf('ArchiveFolder', ''); $aResult['HideUnsubscribed'] = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', $aResult['HideUnsubscribed']); + $aResult['defaultSort'] = (string) $oSettingsLocal->GetConf('defaultSort', $aResult['defaultSort']); $aResult['useThreads'] = (bool) $oSettingsLocal->GetConf('UseThreads', $aResult['useThreads']); $aResult['threadAlgorithm'] = (string) $oSettingsLocal->GetConf('threadAlgorithm', $aResult['threadAlgorithm']); $aResult['ReplySameFolder'] = (bool) $oSettingsLocal->GetConf('ReplySameFolder', $aResult['ReplySameFolder']); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php index 014f403e7..f1a19c172 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php @@ -204,6 +204,7 @@ trait User $this->setSettingsFromParams($oSettings, 'Resizer5Width', 'int'); $this->setSettingsFromParams($oSettings, 'Resizer5Height', 'int'); + $this->setSettingsFromParams($oSettingsLocal, 'defaultSort', 'string'); $this->setSettingsFromParams($oSettingsLocal, 'UseThreads', 'bool'); $this->setSettingsFromParams($oSettingsLocal, 'threadAlgorithm', 'string'); $this->setSettingsFromParams($oSettingsLocal, 'ReplySameFolder', 'bool'); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php index f61af74db..be26a590e 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php @@ -141,21 +141,23 @@ trait UserAuth */ public function LoginProcess(string $sEmail, SensitiveString $oPassword, bool $bMainAccount = true): Account { - $sCredentials = $this->resolveLoginCredentials($sEmail, $oPassword); + $aCredentials = $this->resolveLoginCredentials($sEmail, $oPassword); - if (!\str_contains($sCredentials['email'], '@') || !\strlen($oPassword)) { + if (!\str_contains($aCredentials['email'], '@') || !\strlen($oPassword)) { throw new ClientException(Notifications::InvalidInputArgument); } + $oDomain = $this->DomainProvider()->getByEmailAddress($aCredentials['email']); + $oAccount = null; try { $oAccount = $bMainAccount ? new MainAccount : new AdditionalAccount; $oAccount->setCredentials( - $sCredentials['domain'], - $sCredentials['email'], - $sCredentials['imapUser'], + $aCredentials['domain'], + $aCredentials['email'], + $aCredentials['imapUser'], $oPassword, - $sCredentials['smtpUser'] + $aCredentials['smtpUser'] // ,new SensitiveString($oPassword) ); $this->Plugins()->RunHook('filter.account', array($oAccount)); @@ -207,6 +209,7 @@ trait UserAuth // Test the login $oImapClient = new \MailSo\Imap\ImapClient; + $oImapClient->SetLogger($this->Logger()); $this->imapConnect($oAccount, false, $oImapClient); } $this->SetAdditionalAuthToken($oAccount); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php index 00a0651f9..d3dcd9154 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php @@ -165,7 +165,8 @@ abstract class Account implements \JsonSerializable // $aAccountHash['login'] = $oDomain->ImapSettings()->fixUsername($aAccountHash['login']); $oAccount = new static; $oAccount->sEmail = \SnappyMail\IDN::emailToAscii($aAccountHash['email']); - $oAccount->sImapUser = \SnappyMail\IDN::emailToAscii($aAccountHash['login']); +// $oAccount->sImapUser = \SnappyMail\IDN::emailToAscii($aAccountHash['login']); + $oAccount->sImapUser = $aAccountHash['login']; $oAccount->setImapPass(new SensitiveString($aAccountHash['pass'])); $oAccount->oDomain = $oDomain; $oActions->Plugins()->RunHook('filter.account', array($oAccount)); @@ -286,7 +287,7 @@ abstract class Account implements \JsonSerializable */ /** - * @deprecated + * @deprecated since v2.36.1 */ public function IncLogin() : string { diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php index a65f2235b..b9c7d3c86 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php @@ -106,7 +106,7 @@ class Domain implements \JsonSerializable public function ValidateWhiteList(string $sEmail) : bool { - $sW = \trim($this->whiteList); + $sW = $this->whiteList; if (!$sW) { return true; } @@ -210,7 +210,7 @@ class Domain implements \JsonSerializable $oDomain->SMTP->setSender = !empty($aDomain['smtp_set_sender']); $oDomain->SMTP->usePhpMail = !empty($aDomain['smtp_php_mail']); - $oDomain->whiteList = \trim($aDomain['white_list'] ?? ''); + $oDomain->whiteList = $aDomain['white_list'] ?? ''; $oDomain->Normalize(); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php index c465c19cb..25ef02501 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php @@ -2,19 +2,14 @@ namespace RainLoop\Providers; +use RainLoop\Notifications; use RainLoop\Exceptions\ClientException; class Domain extends AbstractProvider { - /** - * @var Domain\DomainInterface - */ - private $oDriver; + private Domain\DomainInterface $oDriver; - /** - * @var \RainLoop\Plugins\Manager - */ - private $oPlugins; + private \RainLoop\Plugins\Manager $oPlugins; public function __construct(Domain\DomainInterface $oDriver, \RainLoop\Plugins\Manager $oPlugins) { @@ -86,10 +81,10 @@ class Domain extends AbstractProvider { $oDomain = $this->Load(\MailSo\Base\Utils::getEmailAddressDomain($sEmail), true); if (!$oDomain) { - throw new ClientException(Notifications::DomainNotAllowed); + throw new ClientException(Notifications::DomainNotAllowed, null, "{$sEmail} has no domain configuration"); } if (!$oDomain->ValidateWhiteList($sEmail)) { - throw new ClientException(Notifications::AccountNotAllowed); + throw new ClientException(Notifications::AccountNotAllowed, null, "{$sEmail} not whitelisted"); } return $oDomain; } diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component.php index da2c5ebd4..95bde3d40 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component.php @@ -2,7 +2,6 @@ namespace Sabre\VObject; -use Sabre\VObject; use Sabre\Xml; /** @@ -15,7 +14,7 @@ use Sabre\Xml; * @author Evert Pot (http://evertpot.com/) * @license http://sabre.io/license/ Modified BSD License * - * @property VObject\Property\FlatText UID + * @property Property\FlatText UID */ class Component extends Node { diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VAlarm.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VAlarm.php index 3e5797b0a..abe61768e 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VAlarm.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VAlarm.php @@ -39,13 +39,7 @@ class VAlarm extends VObject\Component /** @var VEvent|VTodo $parentComponent */ $parentComponent = $this->parent; if ('START' === $related) { - if ('VTODO' === $parentComponent->name) { - $propName = 'DUE'; - } else { - $propName = 'DTSTART'; - } - - $effectiveTrigger = $parentComponent->$propName->getDateTime(); + $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); } else { if ('VTODO' === $parentComponent->name) { $endProp = 'DUE'; diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCalendar.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCalendar.php index b317e02c8..7b6e66e88 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCalendar.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCalendar.php @@ -20,8 +20,8 @@ use Sabre\VObject\Recur\NoInstancesException; * * @property VEvent VEVENT * @property VJournal VJOURNAL - * @property VObject\Property\Text ORG - * @property VObject\Property\FlatText METHOD + * @property Property\Text ORG + * @property Property\FlatText METHOD */ class VCalendar extends VObject\Document { @@ -51,21 +51,21 @@ class VCalendar extends VObject\Document * List of value-types, and which classes they map to. */ public static array $valueMap = [ - 'BINARY' => VObject\Property\Binary::class, - 'BOOLEAN' => VObject\Property\Boolean::class, - 'CAL-ADDRESS' => VObject\Property\ICalendar\CalAddress::class, - 'DATE' => VObject\Property\ICalendar\Date::class, - 'DATE-TIME' => VObject\Property\ICalendar\DateTime::class, - 'DURATION' => VObject\Property\ICalendar\Duration::class, - 'FLOAT' => VObject\Property\FloatValue::class, - 'INTEGER' => VObject\Property\IntegerValue::class, - 'PERIOD' => VObject\Property\ICalendar\Period::class, - 'RECUR' => VObject\Property\ICalendar\Recur::class, - 'TEXT' => VObject\Property\Text::class, - 'TIME' => VObject\Property\Time::class, - 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only. - 'URI' => VObject\Property\Uri::class, - 'UTC-OFFSET' => VObject\Property\UtcOffset::class, + 'BINARY' => Property\Binary::class, + 'BOOLEAN' => Property\Boolean::class, + 'CAL-ADDRESS' => Property\ICalendar\CalAddress::class, + 'DATE' => Property\ICalendar\Date::class, + 'DATE-TIME' => Property\ICalendar\DateTime::class, + 'DURATION' => Property\ICalendar\Duration::class, + 'FLOAT' => Property\FloatValue::class, + 'INTEGER' => Property\IntegerValue::class, + 'PERIOD' => Property\ICalendar\Period::class, + 'RECUR' => Property\ICalendar\Recur::class, + 'TEXT' => Property\Text::class, + 'TIME' => Property\Time::class, + 'UNKNOWN' => Property\Unknown::class, // jCard / jCal-only. + 'URI' => Property\Uri::class, + 'UTC-OFFSET' => Property\UtcOffset::class, ]; /** @@ -73,78 +73,78 @@ class VCalendar extends VObject\Document */ public static array $propertyMap = [ // Calendar properties - 'CALSCALE' => VObject\Property\FlatText::class, - 'METHOD' => VObject\Property\FlatText::class, - 'PRODID' => VObject\Property\FlatText::class, - 'VERSION' => VObject\Property\FlatText::class, + 'CALSCALE' => Property\FlatText::class, + 'METHOD' => Property\FlatText::class, + 'PRODID' => Property\FlatText::class, + 'VERSION' => Property\FlatText::class, // Component properties - 'ATTACH' => VObject\Property\Uri::class, - 'CATEGORIES' => VObject\Property\Text::class, - 'CLASS' => VObject\Property\FlatText::class, - 'COMMENT' => VObject\Property\FlatText::class, - 'DESCRIPTION' => VObject\Property\FlatText::class, - 'GEO' => VObject\Property\FloatValue::class, - 'LOCATION' => VObject\Property\FlatText::class, - 'PERCENT-COMPLETE' => VObject\Property\IntegerValue::class, - 'PRIORITY' => VObject\Property\IntegerValue::class, - 'RESOURCES' => VObject\Property\Text::class, - 'STATUS' => VObject\Property\FlatText::class, - 'SUMMARY' => VObject\Property\FlatText::class, + 'ATTACH' => Property\Uri::class, + 'CATEGORIES' => Property\Text::class, + 'CLASS' => Property\FlatText::class, + 'COMMENT' => Property\FlatText::class, + 'DESCRIPTION' => Property\FlatText::class, + 'GEO' => Property\FloatValue::class, + 'LOCATION' => Property\FlatText::class, + 'PERCENT-COMPLETE' => Property\IntegerValue::class, + 'PRIORITY' => Property\IntegerValue::class, + 'RESOURCES' => Property\Text::class, + 'STATUS' => Property\FlatText::class, + 'SUMMARY' => Property\FlatText::class, // Date and Time Component Properties - 'COMPLETED' => VObject\Property\ICalendar\DateTime::class, - 'DTEND' => VObject\Property\ICalendar\DateTime::class, - 'DUE' => VObject\Property\ICalendar\DateTime::class, - 'DTSTART' => VObject\Property\ICalendar\DateTime::class, - 'DURATION' => VObject\Property\ICalendar\Duration::class, - 'FREEBUSY' => VObject\Property\ICalendar\Period::class, - 'TRANSP' => VObject\Property\FlatText::class, + 'COMPLETED' => Property\ICalendar\DateTime::class, + 'DTEND' => Property\ICalendar\DateTime::class, + 'DUE' => Property\ICalendar\DateTime::class, + 'DTSTART' => Property\ICalendar\DateTime::class, + 'DURATION' => Property\ICalendar\Duration::class, + 'FREEBUSY' => Property\ICalendar\Period::class, + 'TRANSP' => Property\FlatText::class, // Time Zone Component Properties - 'TZID' => VObject\Property\FlatText::class, - 'TZNAME' => VObject\Property\FlatText::class, - 'TZOFFSETFROM' => VObject\Property\UtcOffset::class, - 'TZOFFSETTO' => VObject\Property\UtcOffset::class, - 'TZURL' => VObject\Property\Uri::class, + 'TZID' => Property\FlatText::class, + 'TZNAME' => Property\FlatText::class, + 'TZOFFSETFROM' => Property\UtcOffset::class, + 'TZOFFSETTO' => Property\UtcOffset::class, + 'TZURL' => Property\Uri::class, // Relationship Component Properties - 'ATTENDEE' => VObject\Property\ICalendar\CalAddress::class, - 'CONTACT' => VObject\Property\FlatText::class, - 'ORGANIZER' => VObject\Property\ICalendar\CalAddress::class, - 'RECURRENCE-ID' => VObject\Property\ICalendar\DateTime::class, - 'RELATED-TO' => VObject\Property\FlatText::class, - 'URL' => VObject\Property\Uri::class, - 'UID' => VObject\Property\FlatText::class, + 'ATTENDEE' => Property\ICalendar\CalAddress::class, + 'CONTACT' => Property\FlatText::class, + 'ORGANIZER' => Property\ICalendar\CalAddress::class, + 'RECURRENCE-ID' => Property\ICalendar\DateTime::class, + 'RELATED-TO' => Property\FlatText::class, + 'URL' => Property\Uri::class, + 'UID' => Property\FlatText::class, // Recurrence Component Properties - 'EXDATE' => VObject\Property\ICalendar\DateTime::class, - 'RDATE' => VObject\Property\ICalendar\DateTime::class, - 'RRULE' => VObject\Property\ICalendar\Recur::class, - 'EXRULE' => VObject\Property\ICalendar\Recur::class, // Deprecated since rfc5545 + 'EXDATE' => Property\ICalendar\DateTime::class, + 'RDATE' => Property\ICalendar\DateTime::class, + 'RRULE' => Property\ICalendar\Recur::class, + 'EXRULE' => Property\ICalendar\Recur::class, // Deprecated since rfc5545 // Alarm Component Properties - 'ACTION' => VObject\Property\FlatText::class, - 'REPEAT' => VObject\Property\IntegerValue::class, - 'TRIGGER' => VObject\Property\ICalendar\Duration::class, + 'ACTION' => Property\FlatText::class, + 'REPEAT' => Property\IntegerValue::class, + 'TRIGGER' => Property\ICalendar\Duration::class, // Change Management Component Properties - 'CREATED' => VObject\Property\ICalendar\DateTime::class, - 'DTSTAMP' => VObject\Property\ICalendar\DateTime::class, - 'LAST-MODIFIED' => VObject\Property\ICalendar\DateTime::class, - 'SEQUENCE' => VObject\Property\IntegerValue::class, + 'CREATED' => Property\ICalendar\DateTime::class, + 'DTSTAMP' => Property\ICalendar\DateTime::class, + 'LAST-MODIFIED' => Property\ICalendar\DateTime::class, + 'SEQUENCE' => Property\IntegerValue::class, // Request Status - 'REQUEST-STATUS' => VObject\Property\Text::class, + 'REQUEST-STATUS' => Property\Text::class, // Additions from draft-daboo-valarm-extensions-04 - 'ALARM-AGENT' => VObject\Property\Text::class, - 'ACKNOWLEDGED' => VObject\Property\ICalendar\DateTime::class, - 'PROXIMITY' => VObject\Property\Text::class, - 'DEFAULT-ALARM' => VObject\Property\Boolean::class, + 'ALARM-AGENT' => Property\Text::class, + 'ACKNOWLEDGED' => Property\ICalendar\DateTime::class, + 'PROXIMITY' => Property\Text::class, + 'DEFAULT-ALARM' => Property\Boolean::class, // Additions from draft-daboo-calendar-availability-05 - 'BUSYTYPE' => VObject\Property\Text::class, + 'BUSYTYPE' => Property\Text::class, ]; /** @@ -166,10 +166,10 @@ class VCalendar extends VObject\Document * * @return VObject\Component[] */ - public function getBaseComponents(string $componentName = null): array + public function getBaseComponents(?string $componentName = null): array { $isBaseComponent = function ($component): bool { - if (!$component instanceof VObject\Component) { + if (!$component instanceof Component) { return false; } if ('VTIMEZONE' === $component->name) { @@ -214,13 +214,11 @@ class VCalendar extends VObject\Document * If there is no such component, null will be returned. * * @param string|null $componentName filter by component name - * - * @return VObject\Component|null */ - public function getBaseComponent(string $componentName = null): ?Component + public function getBaseComponent(?string $componentName = null): ?Component { $isBaseComponent = function ($component): bool { - if (!$component instanceof VObject\Component) { + if (!$component instanceof Component) { return false; } if ('VTIMEZONE' === $component->name) { @@ -275,7 +273,7 @@ class VCalendar extends VObject\Document * @throws InvalidDataException * @throws VObject\Recur\MaxInstancesExceededException */ - public function expand(\DateTimeInterface $start, \DateTimeInterface $end, \DateTimeZone $timeZone = null): VCalendar + public function expand(\DateTimeInterface $start, \DateTimeInterface $end, ?\DateTimeZone $timeZone = null): VCalendar { $newChildren = []; $recurringEvents = []; diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCard.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCard.php index 81f78f422..9ec4c88cc 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCard.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCard.php @@ -82,7 +82,7 @@ class VCard extends VObject\Document 'ROLE' => VObject\Property\FlatText::class, 'LOGO' => VObject\Property\Binary::class, // 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so - // not supported at the moment + // not supported at the moment 'ORG' => VObject\Property\Text::class, 'NOTE' => VObject\Property\FlatText::class, 'REV' => VObject\Property\VCard\TimeStamp::class, diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VTodo.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VTodo.php index bda49da74..cab672ce5 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VTodo.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VTodo.php @@ -104,7 +104,7 @@ class VTodo extends VObject\Component 'LAST-MODIFIED' => '?', 'LOCATION' => '?', 'ORGANIZER' => '?', - 'PERCENT' => '?', + 'PERCENT-COMPLETE' => '?', 'PRIORITY' => '?', 'RECURRENCE-ID' => '?', 'SEQUENCE' => '?', diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/DateTimeParser.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/DateTimeParser.php index 8dbb6cfdb..e8777b7fe 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/DateTimeParser.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/DateTimeParser.php @@ -27,7 +27,7 @@ class DateTimeParser * * @throws InvalidDataException */ - public static function parseDateTime(string $dt, \DateTimeZone $tz = null): \DateTimeImmutable + public static function parseDateTime(string $dt, ?\DateTimeZone $tz = null): \DateTimeImmutable { // Format is YYYYMMDD + "T" + hhmmss $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/', $dt, $matches); @@ -54,7 +54,7 @@ class DateTimeParser * * @throws InvalidDataException */ - public static function parseDate(string $date, \DateTimeZone $tz = null): \DateTimeImmutable + public static function parseDate(string $date, ?\DateTimeZone $tz = null): \DateTimeImmutable { // Format is YYYYMMDD $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/', $date, $matches); @@ -98,12 +98,12 @@ class DateTimeParser } $parts = [ - 'week', - 'day', - 'hour', - 'minute', - 'second', - ]; + 'week', + 'day', + 'hour', + 'minute', + 'second', + ]; foreach ($parts as $part) { $matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0; diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Document.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Document.php index b311987d7..02eaf13c7 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Document.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Document.php @@ -2,8 +2,6 @@ namespace Sabre\VObject; -use Sabre\VObject; - /** * Document. * @@ -18,7 +16,7 @@ use Sabre\VObject; * @author Evert Pot (http://evertpot.com/) * @license http://sabre.io/license/ Modified BSD License * - * @property VObject\Property\FlatText VERSION + * @property Property\FlatText VERSION */ abstract class Document extends Component { @@ -140,7 +138,7 @@ abstract class Document extends Component * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To * ensure that this does not happen, set $defaults to false. */ - public function createComponent(string $name, array $children = null, bool $defaults = true): Component + public function createComponent(string $name, ?array $children = null, bool $defaults = true): Component { $name = strtoupper($name); $class = Component::class; @@ -169,7 +167,7 @@ abstract class Document extends Component * * @throws InvalidDataException */ - public function createProperty(string $name, $value = null, array $parameters = null, string $valueType = null): Property + public function createProperty(string $name, $value = null, ?array $parameters = null, ?string $valueType = null, ?int $lineIndex = null, ?string $lineString = null): Property { // If there's a . in the name, it means it's prefixed by a group name. if (false !== ($i = strpos($name, '.'))) { @@ -203,7 +201,7 @@ abstract class Document extends Component $parameters = []; } - return new $class($this, $name, $value, $parameters, $group); + return new $class($this, $name, $value, $parameters, $group, $lineIndex, $lineString); } /** diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/FreeBusyGenerator.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/FreeBusyGenerator.php index 2f222e4e5..424826034 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/FreeBusyGenerator.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/FreeBusyGenerator.php @@ -72,7 +72,7 @@ class FreeBusyGenerator * Check the setTimeRange and setObjects methods for details about the * arguments. */ - public function __construct(\DateTimeInterface $start = null, \DateTimeInterface $end = null, $objects = null, \DateTimeZone $timeZone = null) + public function __construct(?\DateTimeInterface $start = null, ?\DateTimeInterface $end = null, $objects = null, ?\DateTimeZone $timeZone = null) { $this->setTimeRange($start, $end); @@ -138,7 +138,7 @@ class FreeBusyGenerator * * @throws \Exception */ - public function setTimeRange(\DateTimeInterface $start = null, \DateTimeInterface $end = null): void + public function setTimeRange(?\DateTimeInterface $start = null, ?\DateTimeInterface $end = null): void { if (!$start) { $start = new \DateTimeImmutable(Settings::$minDate); diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ITip/Broker.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ITip/Broker.php index f792fd990..76ee0c71e 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ITip/Broker.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ITip/Broker.php @@ -113,7 +113,7 @@ class Broker * @throws MaxInstancesExceededException * @throws NoInstancesException */ - public function processMessage(Message $itipMessage, VCalendar $existingObject = null) + public function processMessage(Message $itipMessage, ?VCalendar $existingObject = null) { // We only support events at the moment. if ('VEVENT' !== $itipMessage->component) { @@ -268,7 +268,7 @@ class Broker * This is message from an organizer, and is either a new event * invite, or an update to an existing one. */ - protected function processMessageRequest(Message $itipMessage, VCalendar $existingObject = null): ?VCalendar + protected function processMessageRequest(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar { if (!$existingObject) { // This is a new invite, and we're just going to copy over @@ -296,7 +296,7 @@ class Broker * attendee got removed from an event, or an event got cancelled * altogether. */ - protected function processMessageCancel(Message $itipMessage, VCalendar $existingObject = null): ?VCalendar + protected function processMessageCancel(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar { if (!$existingObject) { // The event didn't exist in the first place, so we're just @@ -321,7 +321,7 @@ class Broker * @throws MaxInstancesExceededException * @throws NoInstancesException */ - protected function processMessageReply(Message $itipMessage, VCalendar $existingObject = null): ?VCalendar + protected function processMessageReply(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar { // A reply can only be processed based on an existing object. // If the object is not available, the reply is ignored. @@ -333,7 +333,10 @@ class Broker // Finding all the instances the attendee replied to. foreach ($itipMessage->message->VEVENT as $vevent) { - $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; + // Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence. + // The Unix timestamp will be the same for an event, even if the reply from the attendee + // used a different format/timezone to express the event date-time. + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master'; $attendee = $vevent->ATTENDEE; $instances[$recurId] = $attendee['PARTSTAT']->getValue(); if (isset($vevent->{'REQUEST-STATUS'})) { @@ -346,7 +349,8 @@ class Broker // all the instances where we have a reply for. $masterObject = null; foreach ($existingObject->VEVENT as $vevent) { - $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; + // Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence. + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master'; if ('master' === $recurId) { $masterObject = $vevent; } @@ -393,7 +397,10 @@ class Broker $newObject = $recurrenceIterator->getEventObject(); $recurrenceIterator->next(); - if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getValue() === $recurId) { + // Compare the Unix timestamp returned by getTimestamp with the previously calculated timestamp. + // If they are the same, then this is a matching recurrence, even though its date-time may have + // been expressed in a different format/timezone. + if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() === $recurId) { $found = true; } --$iterations; @@ -496,10 +503,11 @@ class Broker $icalMsg->add(clone $timezone); } - if (!$attendee['newInstances']) { - // If there are no instances the attendee is a part of, it - // means the attendee was removed and we need to send him a - // CANCEL. + if (!$attendee['newInstances'] || 'CANCELLED' === $eventInfo['status']) { + // If there are no instances the attendee is a part of, it means + // the attendee was removed and we need to send them a CANCEL message. + // Also If the meeting STATUS property was changed to CANCELLED + // we need to send the attendee a CANCEL message. $message->method = 'CANCEL'; $icalMsg->METHOD = $message->method; diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/Json.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/Json.php index a732075d5..9729afe86 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/Json.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/Json.php @@ -131,7 +131,7 @@ class Json extends Parser list( $propertyName, $parameters, - $valueType + $valueType, ) = $jProp; $propertyName = strtoupper($propertyName); diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/MimeDir.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/MimeDir.php index bbef0ea6b..be5d87baf 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/MimeDir.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/MimeDir.php @@ -79,6 +79,12 @@ class MimeDir extends Parser $this->setInput($input); } + if (!\is_resource($this->input)) { + // Null was passed as input, but there was no existing input buffer + // There is nothing to parse. + throw new ParseException('No input provided to parse'); + } + if (0 !== $options) { $this->options = $options; } @@ -445,7 +451,7 @@ class MimeDir extends Parser } } - $propObj = $this->root->createProperty($property['name'], null, $namedParameters); + $propObj = $this->root->createProperty($property['name'], null, $namedParameters, null, $this->startLine, $line); foreach ($namelessParameters as $namelessParameter) { $propObj->add(null, $namelessParameter); diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property.php index 53a252fb2..f4d0bdc0b 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property.php @@ -51,6 +51,20 @@ abstract class Property extends Node */ public string $delimiter = ';'; + /** + * The line number in the original iCalendar / vCard file + * that corresponds with the current node + * if the node was read from a file. + */ + public ?int $lineIndex; + + /** + * The line string from the original iCalendar / vCard file + * that corresponds with the current node + * if the node was read from a file. + */ + public ?string $lineString; + /** * Creates the generic property. * @@ -61,7 +75,7 @@ abstract class Property extends Node * @param array $parameters List of parameters * @param string|null $group The vcard property group */ - public function __construct(Component $root, ?string $name, $value = null, array $parameters = [], string $group = null) + public function __construct(Component $root, ?string $name, $value = null, array $parameters = [], ?string $group = null, ?int $lineIndex = null, ?string $lineString = null) { $this->name = $name; $this->group = $group; @@ -75,6 +89,14 @@ abstract class Property extends Node if (!is_null($value)) { $this->setValue($value); } + + if (!is_null($lineIndex)) { + $this->lineIndex = $lineIndex; + } + + if (!is_null($lineString)) { + $this->lineString = $lineString; + } } /** diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/DateTime.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/DateTime.php index 7ee907972..631a73b08 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/DateTime.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/DateTime.php @@ -128,7 +128,7 @@ class DateTime extends Property * * @throws InvalidDataException */ - public function getDateTime(\DateTimeZone $timeZone = null): ?\DateTimeImmutable + public function getDateTime(?\DateTimeZone $timeZone = null): ?\DateTimeImmutable { $dt = $this->getDateTimes($timeZone); if (!$dt) { @@ -149,7 +149,7 @@ class DateTime extends Property * * @throws InvalidDataException */ - public function getDateTimes(\DateTimeZone $timeZone = null): array + public function getDateTimes(?\DateTimeZone $timeZone = null): array { // Does the property have a TZID? /** @var Property\FlatText $tzid */ diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Recur.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Recur.php index 9a1e9e643..10081673e 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Recur.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Recur.php @@ -189,7 +189,14 @@ class Recur extends Property if (empty($part)) { continue; } - list($partName, $partValue) = explode('=', $part); + + $parts = explode('=', $part); + + if (2 !== count($parts)) { + throw new InvalidDataException('The supplied iCalendar RRULE part is incorrect: '.$part); + } + + list($partName, $partValue) = $parts; // The value itself had multiple values.. if (false !== strpos($partValue, ',')) { diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Text.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Text.php index c606ad812..c59029059 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Text.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Text.php @@ -64,7 +64,7 @@ class Text extends Property * @param array $parameters List of parameters * @param string|null $group The vcard property group */ - public function __construct(Component $root, string $name, $value = null, array $parameters = [], string $group = null) + public function __construct(Component $root, string $name, $value = null, array $parameters = [], ?string $group = null) { // There's two types of multi-valued text properties: // 1. multivalue properties. diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/EventIterator.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/EventIterator.php index d79cf3132..2c93bcf4e 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/EventIterator.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/EventIterator.php @@ -89,7 +89,7 @@ class EventIterator implements \Iterator * @throws NoInstancesException * @throws InvalidDataException */ - public function __construct($input, string $uid = null, \DateTimeZone $timeZone = null) + public function __construct($input, ?string $uid = null, ?\DateTimeZone $timeZone = null) { if (is_null($timeZone)) { $timeZone = new \DateTimeZone('UTC'); diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/RRuleIterator.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/RRuleIterator.php index 888556eea..ff276a697 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/RRuleIterator.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/RRuleIterator.php @@ -159,6 +159,14 @@ class RRuleIterator implements \Iterator */ protected ?\DateTimeInterface $currentDate; + /** + * The number of hours that the next occurrence of an event + * jumped forward, usually because summer time started and + * the requested time-of-day like 0230 did not exist on that + * day. And so the event was scheduled 1 hour later at 0330. + */ + protected int $hourJump = 0; + /** * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly, * yearly. @@ -276,12 +284,65 @@ class RRuleIterator implements \Iterator /* Functions that advance the iterator {{{ */ + /** + * Gets the original start time of the RRULE. + * + * The value is formatted as a string with 24-hour:minute:second + */ + protected function startTime(): string + { + return $this->startDate->format('H:i:s'); + } + + /** + * Advances currentDate by the interval. + * The time is set from the original startDate. + * If the recurrence is on a day when summer time started, then the + * time on that day may have jumped forward, for example, from 0230 to 0330. + * Using the original time means that the next recurrence will be calculated + * based on the original start time and the day/week/month/year interval. + * So the start time of the next occurrence can correctly revert to 0230. + */ + protected function advanceTheDate(string $interval): void + { + $this->currentDate = $this->currentDate->modify($interval.' '.$this->startTime()); + } + + /** + * Does the processing for adjusting the time of multi-hourly events when summer time starts. + */ + protected function adjustForTimeJumpsOfHourlyEvent(\DateTimeInterface $previousEventDateTime): void + { + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the next occurrence. + // That happens if the next event time is on a day when summer time starts + // and the event time is in the non-existent hour of the day. + // For example, an event that normally starts at 02:30 will + // have to start at 03:30 on that day. + // If the interval is just 1 hour, then there is no "jumping back" to do. + // The events that day will happen, for example, at 0030 0130 0330 0430 0530... + if ($this->interval > 1) { + $expectedHourOfNextDate = ((int) $previousEventDateTime->format('G') + $this->interval) % 24; + $actualHourOfNextDate = (int) $this->currentDate->format('G'); + $this->hourJump = $actualHourOfNextDate - $expectedHourOfNextDate; + } + } else { + // The hour "jumped" for the previous occurrence, to avoid the non-existent time. + // currentDate got set ahead by (usually) 1 hour on that day. + // Adjust it back for this next occurrence. + $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); + $this->hourJump = 0; + } + } + /** * Does the processing for advancing the iterator for hourly frequency. */ protected function nextHourly(): void { + $previousEventDateTime = clone $this->currentDate; $this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours'); + $this->adjustForTimeJumpsOfHourlyEvent($previousEventDateTime); } /** @@ -290,7 +351,7 @@ class RRuleIterator implements \Iterator protected function nextDaily(): void { if (!$this->byHour && !$this->byDay) { - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); + $this->advanceTheDate('+'.$this->interval.' days'); return; } @@ -349,7 +410,7 @@ class RRuleIterator implements \Iterator protected function nextWeekly(): void { if (!$this->byHour && !$this->byDay) { - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' weeks'); + $this->advanceTheDate('+'.$this->interval.' weeks'); return; } @@ -371,7 +432,7 @@ class RRuleIterator implements \Iterator if ($this->byHour) { $this->currentDate = $this->currentDate->modify('+1 hours'); } else { - $this->currentDate = $this->currentDate->modify('+1 days'); + $this->advanceTheDate('+1 days'); } // Current day of the week @@ -408,13 +469,13 @@ class RRuleIterator implements \Iterator // occur to the next month. We Must skip these invalid // entries. if ($currentDayOfMonth < 29) { - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' months'); + $this->advanceTheDate('+'.$this->interval.' months'); } else { $increase = 0; do { ++$increase; $tempDate = clone $this->currentDate; - $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months'); + $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime()); } while ($tempDate->format('j') != $currentDayOfMonth); $this->currentDate = $tempDate; } @@ -465,11 +526,15 @@ class RRuleIterator implements \Iterator } } + // Set the currentDate to the year and month that we are in, and the day of the month that we have selected. + // That day could be a day when summer time starts, and if the time of the event is, for example, 0230, + // then 0230 will not be a valid time on that day. So always apply the start time from the original startDate. + // The "modify" method will set the time forward to 0330, for example, if needed. $this->currentDate = $this->currentDate->setDate( (int) $this->currentDate->format('Y'), (int) $this->currentDate->format('n'), (int) $occurrence - ); + )->modify($this->startTime()); } /** @@ -586,7 +651,7 @@ class RRuleIterator implements \Iterator } // The easiest form - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' years'); + $this->advanceTheDate('+'.$this->interval.' years'); return; } @@ -610,7 +675,11 @@ class RRuleIterator implements \Iterator // If we advanced to the next month or year, the first // occurrence is always correct. if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { - break 2; + // only consider byMonth matches, + // otherwise, we don't follow RRule correctly + if (in_array($currentMonth, $this->byMonth)) { + break 2; + } } } @@ -646,7 +715,7 @@ class RRuleIterator implements \Iterator (int) $currentYear, (int) $currentMonth, (int) $occurrence - ); + )->modify($this->startTime()); return; } else { @@ -663,7 +732,7 @@ class RRuleIterator implements \Iterator (int) $currentYear, (int) $currentMonth, (int) $currentDayOfMonth - ); + )->modify($this->startTime()); return; } diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Splitter/ICalendar.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Splitter/ICalendar.php index ccf0d1add..79f0bd443 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Splitter/ICalendar.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Splitter/ICalendar.php @@ -46,7 +46,7 @@ class ICalendar implements SplitterInterface { $data = VObject\Reader::read($input, $options); - if (!$data instanceof VObject\Component\VCalendar) { + if (!$data instanceof VCalendar) { throw new VObject\ParseException('Supplied input could not be parsed as VCALENDAR.'); } diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Splitter/VCard.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Splitter/VCard.php index e63ae837d..40775a335 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Splitter/VCard.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Splitter/VCard.php @@ -61,7 +61,7 @@ class VCard implements SplitterInterface try { $object = $this->parser->parse(); - if (!$object instanceof VObject\Component\VCard) { + if (!$object instanceof Component\VCard) { throw new VObject\ParseException('The supplied input contained non-VCARD data.'); } } catch (VObject\EofException $e) { diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimeZoneUtil.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimeZoneUtil.php index 8c2a374fb..2cd458578 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimeZoneUtil.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimeZoneUtil.php @@ -72,7 +72,7 @@ class TimeZoneUtil * Alternatively, if $failIfUncertain is set to true, it will throw an * exception if we cannot accurately determine the timezone. */ - private function findTimeZone(string $tzid, Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone + private function findTimeZone(string $tzid, ?Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone { foreach ($this->timezoneFinders as $timezoneFinder) { $timezone = $timezoneFinder->find($tzid, $failIfUncertain); @@ -117,7 +117,7 @@ class TimeZoneUtil self::getInstance()->addFinder($key, $finder); } - public static function getTimeZone(string $tzid, Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone + public static function getTimeZone(string $tzid, ?Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone { return self::getInstance()->findTimeZone($tzid, $vcalendar, $failIfUncertain); } diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/VCardConverter.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/VCardConverter.php index 88d74602c..ff8fe3f81 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/VCardConverter.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/VCardConverter.php @@ -93,8 +93,8 @@ class VCardConverter ); if (Document::VCARD30 === $targetVersion) { - if ($property instanceof Property\Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) { - /** @var Property\Uri $newProperty */ + if ($property instanceof Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) { + /** @var Uri $newProperty */ $newProperty = $this->convertUriToBinary($output, $newProperty); } elseif ($property instanceof Property\VCard\DateAndOrTime) { // In vCard 4, the birth year may be optional. This is not the @@ -153,8 +153,8 @@ class VCardConverter return; } - if ($property instanceof Property\Binary) { - /** @var Property\Binary $newProperty */ + if ($property instanceof Binary) { + /** @var Binary $newProperty */ $newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters); } elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) { // If a property such as BDAY contained 'X-APPLE-OMIT-YEAR', @@ -256,7 +256,7 @@ class VCardConverter * * @throws InvalidDataException */ - protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters): Uri + protected function convertBinaryToUri(Component\VCard $output, Binary $newProperty, array &$parameters): Uri { $value = $newProperty->getValue(); /** @var Uri $newProperty */ @@ -304,11 +304,11 @@ class VCardConverter * be valid in vCard 3.0 as well, we should convert those to BINARY if * possible, to improve compatibility. * - * @return Property\Binary|Property\Uri|null + * @return Binary|Uri|null * * @throws InvalidDataException */ - protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty): Property + protected function convertUriToBinary(Component\VCard $output, Uri $newProperty): Property { $value = $newProperty->getValue(); diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/windowszones.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/windowszones.php index 2049a95c1..335007983 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/windowszones.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/windowszones.php @@ -11,142 +11,142 @@ */ return [ - 'AUS Central Standard Time' => 'Australia/Darwin', - 'AUS Eastern Standard Time' => 'Australia/Sydney', - 'Afghanistan Standard Time' => 'Asia/Kabul', - 'Alaskan Standard Time' => 'America/Anchorage', - 'Aleutian Standard Time' => 'America/Adak', - 'Altai Standard Time' => 'Asia/Barnaul', - 'Arab Standard Time' => 'Asia/Riyadh', - 'Arabian Standard Time' => 'Asia/Dubai', - 'Arabic Standard Time' => 'Asia/Baghdad', - 'Argentina Standard Time' => 'America/Buenos_Aires', - 'Astrakhan Standard Time' => 'Europe/Astrakhan', - 'Atlantic Standard Time' => 'America/Halifax', - 'Aus Central W. Standard Time' => 'Australia/Eucla', - 'Azerbaijan Standard Time' => 'Asia/Baku', - 'Azores Standard Time' => 'Atlantic/Azores', - 'Bahia Standard Time' => 'America/Bahia', - 'Bangladesh Standard Time' => 'Asia/Dhaka', - 'Belarus Standard Time' => 'Europe/Minsk', - 'Bougainville Standard Time' => 'Pacific/Bougainville', - 'Canada Central Standard Time' => 'America/Regina', - 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', - 'Caucasus Standard Time' => 'Asia/Yerevan', - 'Cen. Australia Standard Time' => 'Australia/Adelaide', - 'Central America Standard Time' => 'America/Guatemala', - 'Central Asia Standard Time' => 'Asia/Almaty', - 'Central Brazilian Standard Time' => 'America/Cuiaba', - 'Central Europe Standard Time' => 'Europe/Budapest', - 'Central European Standard Time' => 'Europe/Warsaw', - 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', - 'Central Standard Time' => 'America/Chicago', - 'Central Standard Time (Mexico)' => 'America/Mexico_City', - 'Chatham Islands Standard Time' => 'Pacific/Chatham', - 'China Standard Time' => 'Asia/Shanghai', - 'Cuba Standard Time' => 'America/Havana', - 'Dateline Standard Time' => 'Etc/GMT+12', - 'E. Africa Standard Time' => 'Africa/Nairobi', - 'E. Australia Standard Time' => 'Australia/Brisbane', - 'E. Europe Standard Time' => 'Europe/Chisinau', - 'E. South America Standard Time' => 'America/Sao_Paulo', - 'Easter Island Standard Time' => 'Pacific/Easter', - 'Eastern Standard Time' => 'America/New_York', - 'Eastern Standard Time (Mexico)' => 'America/Cancun', - 'Egypt Standard Time' => 'Africa/Cairo', - 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', - 'FLE Standard Time' => 'Europe/Kiev', - 'Fiji Standard Time' => 'Pacific/Fiji', - 'GMT Standard Time' => 'Europe/London', - 'GTB Standard Time' => 'Europe/Bucharest', - 'Georgian Standard Time' => 'Asia/Tbilisi', - 'Greenland Standard Time' => 'America/Godthab', - 'Greenwich Standard Time' => 'Atlantic/Reykjavik', - 'Haiti Standard Time' => 'America/Port-au-Prince', - 'Hawaiian Standard Time' => 'Pacific/Honolulu', - 'India Standard Time' => 'Asia/Calcutta', - 'Iran Standard Time' => 'Asia/Tehran', - 'Israel Standard Time' => 'Asia/Jerusalem', - 'Jordan Standard Time' => 'Asia/Amman', - 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', - 'Korea Standard Time' => 'Asia/Seoul', - 'Libya Standard Time' => 'Africa/Tripoli', - 'Line Islands Standard Time' => 'Pacific/Kiritimati', - 'Lord Howe Standard Time' => 'Australia/Lord_Howe', - 'Magadan Standard Time' => 'Asia/Magadan', - 'Magallanes Standard Time' => 'America/Punta_Arenas', - 'Marquesas Standard Time' => 'Pacific/Marquesas', - 'Mauritius Standard Time' => 'Indian/Mauritius', - 'Middle East Standard Time' => 'Asia/Beirut', - 'Montevideo Standard Time' => 'America/Montevideo', - 'Morocco Standard Time' => 'Africa/Casablanca', - 'Mountain Standard Time' => 'America/Denver', - 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', - 'Myanmar Standard Time' => 'Asia/Rangoon', - 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', - 'Namibia Standard Time' => 'Africa/Windhoek', - 'Nepal Standard Time' => 'Asia/Katmandu', - 'New Zealand Standard Time' => 'Pacific/Auckland', - 'Newfoundland Standard Time' => 'America/St_Johns', - 'Norfolk Standard Time' => 'Pacific/Norfolk', - 'North Asia East Standard Time' => 'Asia/Irkutsk', - 'North Asia Standard Time' => 'Asia/Krasnoyarsk', - 'North Korea Standard Time' => 'Asia/Pyongyang', - 'Omsk Standard Time' => 'Asia/Omsk', - 'Pacific SA Standard Time' => 'America/Santiago', - 'Pacific Standard Time' => 'America/Los_Angeles', - 'Pacific Standard Time (Mexico)' => 'America/Tijuana', - 'Pakistan Standard Time' => 'Asia/Karachi', - 'Paraguay Standard Time' => 'America/Asuncion', - 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', - 'Romance Standard Time' => 'Europe/Paris', - 'Russia Time Zone 10' => 'Asia/Srednekolymsk', - 'Russia Time Zone 11' => 'Asia/Kamchatka', - 'Russia Time Zone 3' => 'Europe/Samara', - 'Russian Standard Time' => 'Europe/Moscow', - 'SA Eastern Standard Time' => 'America/Cayenne', - 'SA Pacific Standard Time' => 'America/Bogota', - 'SA Western Standard Time' => 'America/La_Paz', - 'SE Asia Standard Time' => 'Asia/Bangkok', - 'Saint Pierre Standard Time' => 'America/Miquelon', - 'Sakhalin Standard Time' => 'Asia/Sakhalin', - 'Samoa Standard Time' => 'Pacific/Apia', - 'Sao Tome Standard Time' => 'Africa/Sao_Tome', - 'Saratov Standard Time' => 'Europe/Saratov', - 'Singapore Standard Time' => 'Asia/Singapore', - 'South Africa Standard Time' => 'Africa/Johannesburg', - 'Sri Lanka Standard Time' => 'Asia/Colombo', - 'Sudan Standard Time' => 'Africa/Khartoum', - 'Syria Standard Time' => 'Asia/Damascus', - 'Taipei Standard Time' => 'Asia/Taipei', - 'Tasmania Standard Time' => 'Australia/Hobart', - 'Tocantins Standard Time' => 'America/Araguaina', - 'Tokyo Standard Time' => 'Asia/Tokyo', - 'Tomsk Standard Time' => 'Asia/Tomsk', - 'Tonga Standard Time' => 'Pacific/Tongatapu', - 'Transbaikal Standard Time' => 'Asia/Chita', - 'Turkey Standard Time' => 'Europe/Istanbul', - 'Turks And Caicos Standard Time' => 'America/Grand_Turk', - 'US Eastern Standard Time' => 'America/Indianapolis', - 'US Mountain Standard Time' => 'America/Phoenix', - 'UTC' => 'Etc/GMT', - 'UTC+12' => 'Etc/GMT-12', - 'UTC+13' => 'Etc/GMT-13', - 'UTC-02' => 'Etc/GMT+2', - 'UTC-08' => 'Etc/GMT+8', - 'UTC-09' => 'Etc/GMT+9', - 'UTC-11' => 'Etc/GMT+11', - 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', - 'Venezuela Standard Time' => 'America/Caracas', - 'Vladivostok Standard Time' => 'Asia/Vladivostok', - 'Volgograd Standard Time' => 'Europe/Volgograd', - 'W. Australia Standard Time' => 'Australia/Perth', - 'W. Central Africa Standard Time' => 'Africa/Lagos', - 'W. Europe Standard Time' => 'Europe/Berlin', - 'W. Mongolia Standard Time' => 'Asia/Hovd', - 'West Asia Standard Time' => 'Asia/Tashkent', - 'West Bank Standard Time' => 'Asia/Hebron', - 'West Pacific Standard Time' => 'Pacific/Port_Moresby', - 'Yakutsk Standard Time' => 'Asia/Yakutsk', - 'Yukon Standard Time' => 'America/Whitehorse', + 'AUS Central Standard Time' => 'Australia/Darwin', + 'AUS Eastern Standard Time' => 'Australia/Sydney', + 'Afghanistan Standard Time' => 'Asia/Kabul', + 'Alaskan Standard Time' => 'America/Anchorage', + 'Aleutian Standard Time' => 'America/Adak', + 'Altai Standard Time' => 'Asia/Barnaul', + 'Arab Standard Time' => 'Asia/Riyadh', + 'Arabian Standard Time' => 'Asia/Dubai', + 'Arabic Standard Time' => 'Asia/Baghdad', + 'Argentina Standard Time' => 'America/Buenos_Aires', + 'Astrakhan Standard Time' => 'Europe/Astrakhan', + 'Atlantic Standard Time' => 'America/Halifax', + 'Aus Central W. Standard Time' => 'Australia/Eucla', + 'Azerbaijan Standard Time' => 'Asia/Baku', + 'Azores Standard Time' => 'Atlantic/Azores', + 'Bahia Standard Time' => 'America/Bahia', + 'Bangladesh Standard Time' => 'Asia/Dhaka', + 'Belarus Standard Time' => 'Europe/Minsk', + 'Bougainville Standard Time' => 'Pacific/Bougainville', + 'Canada Central Standard Time' => 'America/Regina', + 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', + 'Caucasus Standard Time' => 'Asia/Yerevan', + 'Cen. Australia Standard Time' => 'Australia/Adelaide', + 'Central America Standard Time' => 'America/Guatemala', + 'Central Asia Standard Time' => 'Asia/Almaty', + 'Central Brazilian Standard Time' => 'America/Cuiaba', + 'Central Europe Standard Time' => 'Europe/Budapest', + 'Central European Standard Time' => 'Europe/Warsaw', + 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', + 'Central Standard Time' => 'America/Chicago', + 'Central Standard Time (Mexico)' => 'America/Mexico_City', + 'Chatham Islands Standard Time' => 'Pacific/Chatham', + 'China Standard Time' => 'Asia/Shanghai', + 'Cuba Standard Time' => 'America/Havana', + 'Dateline Standard Time' => 'Etc/GMT+12', + 'E. Africa Standard Time' => 'Africa/Nairobi', + 'E. Australia Standard Time' => 'Australia/Brisbane', + 'E. Europe Standard Time' => 'Europe/Chisinau', + 'E. South America Standard Time' => 'America/Sao_Paulo', + 'Easter Island Standard Time' => 'Pacific/Easter', + 'Eastern Standard Time' => 'America/New_York', + 'Eastern Standard Time (Mexico)' => 'America/Cancun', + 'Egypt Standard Time' => 'Africa/Cairo', + 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', + 'FLE Standard Time' => 'Europe/Kiev', + 'Fiji Standard Time' => 'Pacific/Fiji', + 'GMT Standard Time' => 'Europe/London', + 'GTB Standard Time' => 'Europe/Bucharest', + 'Georgian Standard Time' => 'Asia/Tbilisi', + 'Greenland Standard Time' => 'America/Godthab', + 'Greenwich Standard Time' => 'Atlantic/Reykjavik', + 'Haiti Standard Time' => 'America/Port-au-Prince', + 'Hawaiian Standard Time' => 'Pacific/Honolulu', + 'India Standard Time' => 'Asia/Calcutta', + 'Iran Standard Time' => 'Asia/Tehran', + 'Israel Standard Time' => 'Asia/Jerusalem', + 'Jordan Standard Time' => 'Asia/Amman', + 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', + 'Korea Standard Time' => 'Asia/Seoul', + 'Libya Standard Time' => 'Africa/Tripoli', + 'Line Islands Standard Time' => 'Pacific/Kiritimati', + 'Lord Howe Standard Time' => 'Australia/Lord_Howe', + 'Magadan Standard Time' => 'Asia/Magadan', + 'Magallanes Standard Time' => 'America/Punta_Arenas', + 'Marquesas Standard Time' => 'Pacific/Marquesas', + 'Mauritius Standard Time' => 'Indian/Mauritius', + 'Middle East Standard Time' => 'Asia/Beirut', + 'Montevideo Standard Time' => 'America/Montevideo', + 'Morocco Standard Time' => 'Africa/Casablanca', + 'Mountain Standard Time' => 'America/Denver', + 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', + 'Myanmar Standard Time' => 'Asia/Rangoon', + 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', + 'Namibia Standard Time' => 'Africa/Windhoek', + 'Nepal Standard Time' => 'Asia/Katmandu', + 'New Zealand Standard Time' => 'Pacific/Auckland', + 'Newfoundland Standard Time' => 'America/St_Johns', + 'Norfolk Standard Time' => 'Pacific/Norfolk', + 'North Asia East Standard Time' => 'Asia/Irkutsk', + 'North Asia Standard Time' => 'Asia/Krasnoyarsk', + 'North Korea Standard Time' => 'Asia/Pyongyang', + 'Omsk Standard Time' => 'Asia/Omsk', + 'Pacific SA Standard Time' => 'America/Santiago', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Pacific Standard Time (Mexico)' => 'America/Tijuana', + 'Pakistan Standard Time' => 'Asia/Karachi', + 'Paraguay Standard Time' => 'America/Asuncion', + 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', + 'Romance Standard Time' => 'Europe/Paris', + 'Russia Time Zone 10' => 'Asia/Srednekolymsk', + 'Russia Time Zone 11' => 'Asia/Kamchatka', + 'Russia Time Zone 3' => 'Europe/Samara', + 'Russian Standard Time' => 'Europe/Moscow', + 'SA Eastern Standard Time' => 'America/Cayenne', + 'SA Pacific Standard Time' => 'America/Bogota', + 'SA Western Standard Time' => 'America/La_Paz', + 'SE Asia Standard Time' => 'Asia/Bangkok', + 'Saint Pierre Standard Time' => 'America/Miquelon', + 'Sakhalin Standard Time' => 'Asia/Sakhalin', + 'Samoa Standard Time' => 'Pacific/Apia', + 'Sao Tome Standard Time' => 'Africa/Sao_Tome', + 'Saratov Standard Time' => 'Europe/Saratov', + 'Singapore Standard Time' => 'Asia/Singapore', + 'South Africa Standard Time' => 'Africa/Johannesburg', + 'Sri Lanka Standard Time' => 'Asia/Colombo', + 'Sudan Standard Time' => 'Africa/Khartoum', + 'Syria Standard Time' => 'Asia/Damascus', + 'Taipei Standard Time' => 'Asia/Taipei', + 'Tasmania Standard Time' => 'Australia/Hobart', + 'Tocantins Standard Time' => 'America/Araguaina', + 'Tokyo Standard Time' => 'Asia/Tokyo', + 'Tomsk Standard Time' => 'Asia/Tomsk', + 'Tonga Standard Time' => 'Pacific/Tongatapu', + 'Transbaikal Standard Time' => 'Asia/Chita', + 'Turkey Standard Time' => 'Europe/Istanbul', + 'Turks And Caicos Standard Time' => 'America/Grand_Turk', + 'US Eastern Standard Time' => 'America/Indianapolis', + 'US Mountain Standard Time' => 'America/Phoenix', + 'UTC' => 'Etc/GMT', + 'UTC+12' => 'Etc/GMT-12', + 'UTC+13' => 'Etc/GMT-13', + 'UTC-02' => 'Etc/GMT+2', + 'UTC-08' => 'Etc/GMT+8', + 'UTC-09' => 'Etc/GMT+9', + 'UTC-11' => 'Etc/GMT+11', + 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', + 'Venezuela Standard Time' => 'America/Caracas', + 'Vladivostok Standard Time' => 'Asia/Vladivostok', + 'Volgograd Standard Time' => 'Europe/Volgograd', + 'W. Australia Standard Time' => 'Australia/Perth', + 'W. Central Africa Standard Time' => 'Africa/Lagos', + 'W. Europe Standard Time' => 'Europe/Berlin', + 'W. Mongolia Standard Time' => 'Asia/Hovd', + 'West Asia Standard Time' => 'Asia/Tashkent', + 'West Bank Standard Time' => 'Asia/Hebron', + 'West Pacific Standard Time' => 'Pacific/Port_Moresby', + 'Yakutsk Standard Time' => 'Asia/Yakutsk', + 'Yukon Standard Time' => 'America/Whitehorse', ]; diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/ContextStackTrait.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/ContextStackTrait.php index b514f3754..7c082f5fb 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/ContextStackTrait.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/ContextStackTrait.php @@ -110,7 +110,7 @@ trait ContextStackTrait $this->elementMap, $this->contextUri, $this->namespaceMap, - $this->classMap + $this->classMap, ) = array_pop($this->contextStack); } } diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Service.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Service.php index ad18b6f4e..6e225bdfd 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Service.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Service.php @@ -109,10 +109,17 @@ class Service */ public function parse($input, ?string $contextUri = null, ?string &$rootElementName = null) { - if (is_resource($input)) { + if (!is_string($input)) { // Unfortunately the XMLReader doesn't support streams. When it // does, we can optimize this. - $input = (string) stream_get_contents($input); + if (is_resource($input)) { + $input = (string) stream_get_contents($input); + } else { + // Input is not a string and not a resource. + // Therefore, it has to be a closed resource. + // Effectively empty input has been passed in. + $input = ''; + } } // If input is empty, then it's safe to throw an exception @@ -153,10 +160,17 @@ class Service */ public function expect($rootElementName, $input, ?string $contextUri = null) { - if (is_resource($input)) { + if (!is_string($input)) { // Unfortunately the XMLReader doesn't support streams. When it // does, we can optimize this. - $input = (string) stream_get_contents($input); + if (is_resource($input)) { + $input = (string) stream_get_contents($input); + } else { + // Input is not a string and not a resource. + // Therefore, it has to be a closed resource. + // Effectively empty input has been passed in. + $input = ''; + } } // If input is empty, then it's safe to throw an exception diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Version.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Version.php index f38e87c27..340393169 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Version.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Version.php @@ -16,5 +16,5 @@ class Version /** * Full version number. */ - public const VERSION = '4.0.5'; + public const VERSION = '4.0.6'; } diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Writer.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Writer.php index 77bdae02c..d2fb04ccd 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Writer.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Writer.php @@ -236,7 +236,7 @@ class Writer extends \XMLWriter list( $namespace, - $localName + $localName, ) = Service::parseClarkNotation($name); if (array_key_exists($namespace, $this->namespaceMap)) { diff --git a/snappymail/v/0.0.0/app/localization/ar/user.json b/snappymail/v/0.0.0/app/localization/ar/user.json index 676857698..513560340 100644 --- a/snappymail/v/0.0.0/app/localization/ar/user.json +++ b/snappymail/v/0.0.0/app/localization/ar/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Using %SIZE% (%PROC%%)<\/strong> of your %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/be/user.json b/snappymail/v/0.0.0/app/localization/be/user.json index 6e2fa9f5d..c56f6f005 100644 --- a/snappymail/v/0.0.0/app/localization/be/user.json +++ b/snappymail/v/0.0.0/app/localization/be/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Выкарыстана %SIZE% (%PROC%%)<\/strong> з %LIMIT%<\/strong>", "SORT": "Сартаванне", "NO_SORT": "Паштовы сервер не падтрымлівае сартаванне", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Дата па ўзрастанні", "SORT_DATE_DESC": "Дата па спаданні", "SORT_SIZE_ASC": "Памер па ўзрастанні", diff --git a/snappymail/v/0.0.0/app/localization/bg/user.json b/snappymail/v/0.0.0/app/localization/bg/user.json index dbecf23d7..a64fedd65 100644 --- a/snappymail/v/0.0.0/app/localization/bg/user.json +++ b/snappymail/v/0.0.0/app/localization/bg/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Използване на %SIZE% (%PROC%%)<\/strong> от вашите %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/cs/user.json b/snappymail/v/0.0.0/app/localization/cs/user.json index 724217cc7..b42c93de7 100644 --- a/snappymail/v/0.0.0/app/localization/cs/user.json +++ b/snappymail/v/0.0.0/app/localization/cs/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Používáte %SIZE% (%PROC%%)<\/strong> z vašich %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/da/user.json b/snappymail/v/0.0.0/app/localization/da/user.json index bc246b57b..b2d247392 100644 --- a/snappymail/v/0.0.0/app/localization/da/user.json +++ b/snappymail/v/0.0.0/app/localization/da/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Du bruger %SIZE% (%PROC%%)<\/strong> af %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/de/user.json b/snappymail/v/0.0.0/app/localization/de/user.json index 3869a6065..fcc16a687 100644 --- a/snappymail/v/0.0.0/app/localization/de/user.json +++ b/snappymail/v/0.0.0/app/localization/de/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Sie nutzen %SIZE% (%PROC%%)<\/strong> von %LIMIT%<\/strong>.", "SORT": "Sortieren", "NO_SORT": "Ihr Mailserver unterstützt das Sortieren von E-Mails nicht", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Datum aufsteigend", "SORT_DATE_DESC": "Datum absteigend", "SORT_SIZE_ASC": "Größe aufsteigend", diff --git a/snappymail/v/0.0.0/app/localization/el/user.json b/snappymail/v/0.0.0/app/localization/el/user.json index 0223b0029..983250946 100644 --- a/snappymail/v/0.0.0/app/localization/el/user.json +++ b/snappymail/v/0.0.0/app/localization/el/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Χρησιμοποιούνται %SIZE% (%PROC%%)<\/strong> από %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/en/user.json b/snappymail/v/0.0.0/app/localization/en/user.json index 2439964e3..ef8da1452 100644 --- a/snappymail/v/0.0.0/app/localization/en/user.json +++ b/snappymail/v/0.0.0/app/localization/en/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Using %SIZE% (%PROC%%)<\/strong> of your %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/es/user.json b/snappymail/v/0.0.0/app/localization/es/user.json index ef00f08a2..34768fc30 100644 --- a/snappymail/v/0.0.0/app/localization/es/user.json +++ b/snappymail/v/0.0.0/app/localization/es/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Usando %SIZE% (%PROC%%)<\/strong> de sus %LIMIT%<\/strong>", "SORT": "Ordenar", "NO_SORT": "El servidor no soporta clasificación", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Fecha ascendente", "SORT_DATE_DESC": "Fecha descendente", "SORT_SIZE_ASC": "Tamaño ascendente", diff --git a/snappymail/v/0.0.0/app/localization/et/user.json b/snappymail/v/0.0.0/app/localization/et/user.json index c66d9cbbc..bcca7ee88 100644 --- a/snappymail/v/0.0.0/app/localization/et/user.json +++ b/snappymail/v/0.0.0/app/localization/et/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Kasutad %SIZE% (%PROC%%)<\/strong> lubatud kettamahust %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/eu/user.json b/snappymail/v/0.0.0/app/localization/eu/user.json index dca831e9f..ab073e58a 100644 --- a/snappymail/v/0.0.0/app/localization/eu/user.json +++ b/snappymail/v/0.0.0/app/localization/eu/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "%SIZE% (%PROC%%)<\/strong> erabiliz zure %LIMIT%<\/strong>-tik", "SORT": "Ordenatu", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Data goraka", "SORT_DATE_DESC": "Data beheraka", "SORT_SIZE_ASC": "Tamaina goraka", diff --git a/snappymail/v/0.0.0/app/localization/fa/user.json b/snappymail/v/0.0.0/app/localization/fa/user.json index 75592f471..75bf574bd 100644 --- a/snappymail/v/0.0.0/app/localization/fa/user.json +++ b/snappymail/v/0.0.0/app/localization/fa/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "استفاده %SIZE% (%PROC%%)<\/strong> از%LIMIT%<\/strong> برای شما", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/fi/user.json b/snappymail/v/0.0.0/app/localization/fi/user.json index f14ed8416..0a2f84b1f 100644 --- a/snappymail/v/0.0.0/app/localization/fi/user.json +++ b/snappymail/v/0.0.0/app/localization/fi/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Käytät %SIZE% (%PROC%%)<\/strong> sinulle varatusta %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/fr/user.json b/snappymail/v/0.0.0/app/localization/fr/user.json index 1df10a859..08a5b3dee 100644 --- a/snappymail/v/0.0.0/app/localization/fr/user.json +++ b/snappymail/v/0.0.0/app/localization/fr/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Vous utilisez %SIZE% (%PROC%%)<\/strong> sur %LIMIT%<\/strong>", "SORT": "Trier", "NO_SORT": "Le serveur de messagerie ne prend pas en charge le tri", + "SORT_ARRIVAL_ASC": "Arrivée ascendante", + "SORT_ARRIVAL_DESC": "Arrivée décroissante", "SORT_DATE_ASC": "Date ascendante", "SORT_DATE_DESC": "Date décroissante", "SORT_SIZE_ASC": "Taille ascendante", diff --git a/snappymail/v/0.0.0/app/localization/hu/user.json b/snappymail/v/0.0.0/app/localization/hu/user.json index 94e4bf9f6..15d1f3d60 100644 --- a/snappymail/v/0.0.0/app/localization/hu/user.json +++ b/snappymail/v/0.0.0/app/localization/hu/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Felhasználva %SIZE% (%PROC%%)<\/strong> ebből: %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/id/user.json b/snappymail/v/0.0.0/app/localization/id/user.json index 4c02611c6..f789ddf95 100644 --- a/snappymail/v/0.0.0/app/localization/id/user.json +++ b/snappymail/v/0.0.0/app/localization/id/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Menggunakan %SIZE% (%PROC%%)<\/strong> dari %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/is/user.json b/snappymail/v/0.0.0/app/localization/is/user.json index bbf9b8da0..33e7384db 100644 --- a/snappymail/v/0.0.0/app/localization/is/user.json +++ b/snappymail/v/0.0.0/app/localization/is/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Notar %SIZE% (%PROC%%)<\/strong> af %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/it/user.json b/snappymail/v/0.0.0/app/localization/it/user.json index 3829b1444..92c89fb30 100644 --- a/snappymail/v/0.0.0/app/localization/it/user.json +++ b/snappymail/v/0.0.0/app/localization/it/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Stai usando %SIZE% (%PROC%%)<\/strong> su %LIMIT%<\/strong>", "SORT": "Riordina", "NO_SORT": "Il mail server non supporta l'ordinamento", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Data crescente", "SORT_DATE_DESC": "Data decrescente", "SORT_SIZE_ASC": "Dimensione crescente", diff --git a/snappymail/v/0.0.0/app/localization/ja/user.json b/snappymail/v/0.0.0/app/localization/ja/user.json index 1bcab0ebc..6eb886d48 100644 --- a/snappymail/v/0.0.0/app/localization/ja/user.json +++ b/snappymail/v/0.0.0/app/localization/ja/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "%SIZE% (%PROC%%)<\/strong>\/%LIMIT%<\/strong>を使用中", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "日付で昇順", "SORT_DATE_DESC": "日付で降順", "SORT_SIZE_ASC": "サイズで昇順", diff --git a/snappymail/v/0.0.0/app/localization/ko/user.json b/snappymail/v/0.0.0/app/localization/ko/user.json index 02b0bb0d5..fd50e6f09 100644 --- a/snappymail/v/0.0.0/app/localization/ko/user.json +++ b/snappymail/v/0.0.0/app/localization/ko/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "총 %LIMIT%<\/strong> 중 %PROC%%<\/strong>을(를) 사용 중입니다.", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/lt/user.json b/snappymail/v/0.0.0/app/localization/lt/user.json index 483b0a2b6..3e94f5ac8 100644 --- a/snappymail/v/0.0.0/app/localization/lt/user.json +++ b/snappymail/v/0.0.0/app/localization/lt/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Naudojate %SIZE% (%PROC%%)<\/strong> iš savo turimų %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/lv/user.json b/snappymail/v/0.0.0/app/localization/lv/user.json index 20ed39cc2..3c75f0eb2 100644 --- a/snappymail/v/0.0.0/app/localization/lv/user.json +++ b/snappymail/v/0.0.0/app/localization/lv/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Izmantots %SIZE% (%PROC%%)<\/strong> no %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/nb/user.json b/snappymail/v/0.0.0/app/localization/nb/user.json index fa14ab14b..dd2a2347b 100644 --- a/snappymail/v/0.0.0/app/localization/nb/user.json +++ b/snappymail/v/0.0.0/app/localization/nb/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Bruker %SIZE% ( %PROC%% ) <\/strong> av %LIMIT% <\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Dato, stigende", "SORT_DATE_DESC": "Dato, synkende", "SORT_SIZE_ASC": "Størrelse, stigende", diff --git a/snappymail/v/0.0.0/app/localization/nl/user.json b/snappymail/v/0.0.0/app/localization/nl/user.json index 5c898ef85..e94df562b 100644 --- a/snappymail/v/0.0.0/app/localization/nl/user.json +++ b/snappymail/v/0.0.0/app/localization/nl/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "U gebruikt %SIZE% (%PROC%%)<\/strong> van %LIMIT%<\/strong>", "SORT": "Sorteren", "NO_SORT": "Mail server ondersteund sorteren niet", + "SORT_ARRIVAL_ASC": "Ontvangen oplopend", + "SORT_ARRIVAL_DESC": "Ontvangen aflopend", "SORT_DATE_ASC": "Datum oplopend", "SORT_DATE_DESC": "Datum aflopend", "SORT_SIZE_ASC": "Grootte oplopend", diff --git a/snappymail/v/0.0.0/app/localization/pl/admin.json b/snappymail/v/0.0.0/app/localization/pl/admin.json index 6066b3575..855374b15 100644 --- a/snappymail/v/0.0.0/app/localization/pl/admin.json +++ b/snappymail/v/0.0.0/app/localization/pl/admin.json @@ -136,11 +136,11 @@ "SECURE_OPTION_NONE": "Bez zabezpieczeń", "LABEL_ALLOW_SIEVE_SCRIPTS": "Zezwól na skrypty sieve", "LABEL_USE_SHORT_LOGIN": "Użyj krótkiego loginu", - "LABEL_LOWERCASE_LOGIN": "Lowercase login (case-insensitive)", + "LABEL_LOWERCASE_LOGIN": "Logowanie małymi literami (bez uwzględniania ich wielkości)", "LABEL_USE_AUTH": "Użyj uwierzytelnienia", "LABEL_SET_SENDER": "Użyj loginu jako nadawcy", "LABEL_AUTH_PLAIN_LINE": "Wymuś logowanie pojedynczą komendą AUTH PLAIN", - "LABEL_USE_PHP_MAIL": "Użyj funkcji php 'mail()'", + "LABEL_USE_PHP_MAIL": "Użyj funkcji PHP 'mail()'", "BUTTON_WHITE_LIST": "Biała lista", "NEW_DOMAIN_DESC": "Konfiguracja tej domeny pozwala na pracę z adresami e-mail: %NAME%<\/strong>", "WHITE_LIST_ALERT": "Lista użytkowników domeny, którzy mogą uzyskać dostęp z tego webmaila.\nUżyj spacji do rozdzielenia.\n", diff --git a/snappymail/v/0.0.0/app/localization/pl/user.json b/snappymail/v/0.0.0/app/localization/pl/user.json index 24ea42e1e..47969b3d1 100644 --- a/snappymail/v/0.0.0/app/localization/pl/user.json +++ b/snappymail/v/0.0.0/app/localization/pl/user.json @@ -89,18 +89,18 @@ "ARCHIVE_NAME": "Archiwum" }, "IMAP": { - "ACL_RIGHTS": "Rights", - "ACL_A": "Administer", - "ACL_E": "Expunge", - "ACL_I": "Insert messages", - "ACL_K": "Create sub-folders", - "ACL_L": "Show messages", - "ACL_P": "Send mail", - "ACL_R": "Select folder", - "ACL_S": "\\SEEN tag", - "ACL_T": "\\DELETED tag", - "ACL_W": "Other tags", - "ACL_X": "Delete\/Rename folder" + "ACL_RIGHTS": "Uprawnienia", + "ACL_A": "Administracja", + "ACL_E": "Usuń", + "ACL_I": "Wstaw wiadomości", + "ACL_K": "Utwórz podfoldery", + "ACL_L": "Pokaż wiadomości", + "ACL_P": "Wyślij mail", + "ACL_R": "Wybierz folder", + "ACL_S": "\\Tag przeczytane", + "ACL_T": "\\Tag usunięte", + "ACL_W": "Inne tagi", + "ACL_X": "Usuń\/Zmień nazwę folder(-u)" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Odśwież listę wiadomości", @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Wykorzystano %SIZE% (%PROC%%)<\/strong> z %LIMIT%<\/strong>", "SORT": "Sortowanie", "NO_SORT": "Serwer pocztowy nie wspiera sortowania", + "SORT_ARRIVAL_ASC": "Odebrano rosnąco", + "SORT_ARRIVAL_DESC": "Odebrano malejąco", "SORT_DATE_ASC": "Data rosnąco", "SORT_DATE_DESC": "Data malejąco", "SORT_SIZE_ASC": "Wielkość rosnąco", @@ -219,9 +221,9 @@ "DISPLAY_NAME": "Nazwa wyświetlana", "LAST_NAME": "Nazwisko", "FIRST_NAME": "Imię", - "MIDDLE_NAME": "Middle name", - "NAME_PREFIX": "Name prefix", - "NAME_SUFFIX": "Name suffix", + "MIDDLE_NAME": "Drugie imię", + "NAME_PREFIX": "Przedrostek imienia", + "NAME_SUFFIX": "Przyrostek imienia", "NICK_NAME": "Pseudonim", "LABEL_READ_ONLY": "Tylko do odczytu", "ADD_MENU_LABEL": "Dodaj", @@ -537,7 +539,7 @@ "TO_MANY_FOLDERS_DESC_2": "Pokazaliśmy tylko część z nich aby uniknąć problemów z wydajnością.", "HELP_DELETE_FOLDER": "Usuń folder", "HELP_SHOW_HIDE_FOLDER": "Pokaż\/ukryj folder", - "HELP_CHECK_FOR_NEW_MESSAGES": "Sprawdzaj nowych wiadomości", + "HELP_CHECK_FOR_NEW_MESSAGES": "Sprawdź czy są nowe wiadomości", "HIDE_UNSUBSCRIBED": "Ukryj niesubskrybowane foldery", "UNHIDE_KOLAB_FOLDERS": "Pokaż foldery Kolab", "TYPE_CALENDAR": "Kalendarz", diff --git a/snappymail/v/0.0.0/app/localization/pt-BR/user.json b/snappymail/v/0.0.0/app/localization/pt-BR/user.json index 57843d841..c188a135a 100644 --- a/snappymail/v/0.0.0/app/localization/pt-BR/user.json +++ b/snappymail/v/0.0.0/app/localization/pt-BR/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Utilizando %SIZE% (%PROC%%)<\/strong> de %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", @@ -219,9 +221,9 @@ "DISPLAY_NAME": "Nome de exibição", "LAST_NAME": "Sobrenome", "FIRST_NAME": "Primeiro nome", - "MIDDLE_NAME": "Middle name", - "NAME_PREFIX": "Name prefix", - "NAME_SUFFIX": "Name suffix", + "MIDDLE_NAME": "Nomes adicionais", + "NAME_PREFIX": "Prefixo do nome", + "NAME_SUFFIX": "Sufixo do nome", "NICK_NAME": "Apelido", "LABEL_READ_ONLY": "Somente Leitura", "ADD_MENU_LABEL": "Adicionar", diff --git a/snappymail/v/0.0.0/app/localization/pt/user.json b/snappymail/v/0.0.0/app/localization/pt/user.json index da1ed6de6..a32437267 100644 --- a/snappymail/v/0.0.0/app/localization/pt/user.json +++ b/snappymail/v/0.0.0/app/localization/pt/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Está a usar %SIZE% (%PROC%%)<\/strong> dos %LIMIT%<\/strong>", "SORT": "Ordenar", "NO_SORT": "O servidor de correio não suporta ordenação", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Data ascendente", "SORT_DATE_DESC": "Data descendente", "SORT_SIZE_ASC": "Tamanho ascendente", diff --git a/snappymail/v/0.0.0/app/localization/ro/user.json b/snappymail/v/0.0.0/app/localization/ro/user.json index e82b3080a..64491adc0 100644 --- a/snappymail/v/0.0.0/app/localization/ro/user.json +++ b/snappymail/v/0.0.0/app/localization/ro/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Ați folosit %SIZE% (%PROC%%)<\/strong> din %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/ru/user.json b/snappymail/v/0.0.0/app/localization/ru/user.json index e3bd2dfad..2a0a16c18 100644 --- a/snappymail/v/0.0.0/app/localization/ru/user.json +++ b/snappymail/v/0.0.0/app/localization/ru/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Использовано %SIZE% (%PROC%%)<\/strong> из %LIMIT%<\/strong>", "SORT": "Сортировка", "NO_SORT": "Почтовый сервер не поддерживает сортировку", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Дата по возрастанию", "SORT_DATE_DESC": "Дата по убыванию", "SORT_SIZE_ASC": "Размер по возрастанию", diff --git a/snappymail/v/0.0.0/app/localization/sk/user.json b/snappymail/v/0.0.0/app/localization/sk/user.json index 520cfe073..aac2e35dd 100644 --- a/snappymail/v/0.0.0/app/localization/sk/user.json +++ b/snappymail/v/0.0.0/app/localization/sk/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Používate %SIZE% (%PROC%%)<\/strong> z vašich %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/sl/user.json b/snappymail/v/0.0.0/app/localization/sl/user.json index e1c9116f5..805ec438b 100644 --- a/snappymail/v/0.0.0/app/localization/sl/user.json +++ b/snappymail/v/0.0.0/app/localization/sl/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Uporabljenih %SIZE% (%PROC%%)<\/strong> od %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/sv/user.json b/snappymail/v/0.0.0/app/localization/sv/user.json index 07e071708..da15f2e46 100644 --- a/snappymail/v/0.0.0/app/localization/sv/user.json +++ b/snappymail/v/0.0.0/app/localization/sv/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Använder %SIZE% (%PROC%%)<\/strong> av dina %LIMIT%<\/strong>", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/tr/user.json b/snappymail/v/0.0.0/app/localization/tr/user.json index dfa6a222e..46dd4bad4 100644 --- a/snappymail/v/0.0.0/app/localization/tr/user.json +++ b/snappymail/v/0.0.0/app/localization/tr/user.json @@ -130,6 +130,8 @@ "QUOTA_SIZE": "Size ayrılan %LIMIT%<\/strong>'dan %SIZE% (%PROC%%)<\/strong> kullanıyorsunuz", "SORT": "Sort", "NO_SORT": "Mail server does not support sorting", + "SORT_ARRIVAL_ASC": "Arrived ascending", + "SORT_ARRIVAL_DESC": "Arrived descending", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", diff --git a/snappymail/v/0.0.0/app/localization/uk/admin.json b/snappymail/v/0.0.0/app/localization/uk/admin.json new file mode 100644 index 000000000..d32964f95 --- /dev/null +++ b/snappymail/v/0.0.0/app/localization/uk/admin.json @@ -0,0 +1,182 @@ +{ + "GLOBAL": { + "ADD": "Додати", + "CLOSE": "Закрити", + "SAVE": "Зберегти", + "TEST": "Тест", + "UPDATE": "Оновити", + "USERNAME": "Ім'я користувача", + "PASSWORD": "Пароль", + "CANCEL": "Скасувати", + "SEARCH": "Пошук", + "YES": "Так", + "NO": "Ні" + }, + "LOGIN": { + "LABEL_LOGIN": "Логін", + "LABEL_TOTP": "Код підтвердження TOTP", + "BUTTON_LOGIN": "Увійти в панель адміністратора" + }, + "TOP_PANEL": { + "LABEL_ADMIN_PANEL": "Панель управління адміністратора" + }, + "SETTINGS_LABELS": { + "GENERAL": "Загальні", + "LOGIN": "Вхід", + "BRANDING": "Брендінг", + "CONTACTS": "Контакти", + "DOMAINS": "Домени", + "SECURITY": "Безпека", + "PACKAGES": "Плагіни", + "CONFIG": "Файл конфігурації", + "ABOUT": "Про програму" + }, + "TAB_GENERAL": { + "LEGEND_INTERFACE": "Інтерфейс", + "LABEL_LANGUAGE": "Мова", + "LABEL_LANGUAGE_ADMIN": "Мова (Панелі Адміністратора)", + "LABEL_THEME": "Тема за замовчуванням", + "LABEL_ALLOW_LANGUAGES_ON_SETTINGS": "Дозволити користувачу вибір мови", + "LABEL_ALLOW_THEMES_ON_SETTINGS": "Дозволити користувачу вибір теми", + "LABEL_ALLOW_BACKGROUND_ON_SETTINGS": "Дозволити користувачу вибір фону", + "LABEL_SHOW_THUMBNAILS": "Показувати мініатюри (для вкладень)", + "LEGEND_MAIN": "Основне", + "LABEL_ATTACHMENT_SIZE_LIMIT": "Ліміт розміру вкладень", + "LABEL_ALLOW_ADDITIONAL_ACCOUNTS": "Дозволити додаткові облікові записи", + "LABEL_ALLOW_IDENTITIES": "Дозволити кілька варіантів відправника", + "ALERT_DATA_ACCESS": "Папка даних SnappyMail доступна. Будь ласка, налаштуйте свій веб-сервер так, щоб приховати папку даних із зовнішнього доступу. Детальніше тут:", + "ALERT_WARNING": "Увага!", + "HTML_ALERT_WEAK_PASSWORD": "Ви використовуєте стандартний пароль адміністратора.\n
\nЗ міркувань безпеки, будь ласка,\nзмініть пароль<\/a><\/strong> просто зараз.\n" + }, + "TAB_LOGIN": { + "LEGEND_LOGIN_SCREEN": "Сторінка Входу", + "LABEL_DEFAULT_DOMAIN": "Основний домен", + "LABEL_DETERMINE_USER_DOMAIN": "Намагатися визначити домен користувача", + "LABEL_ALLOW_LANGUAGES_ON_LOGIN": "Дозволити вибір мови на сторінці входу", + "LABEL_DETERMINE_USER_LANGUAGE": "Намагатися визначити мову користувача" + }, + "TAB_BRANDING": { + "LEGEND_BRANDING": "Брендінг", + "LABEL_PAGE_TITLE": "Назва сторінки (Title)", + "LABEL_LOADING_DESCRIPTION": "Назва при завантаженні та на сторінці входу", + "LABEL_FAVICON_URL": "Значок (favicon)" + }, + "TAB_CONTACTS": { + "LEGEND_CONTACTS": "Контакти", + "LEGEND_STORAGE": "Сховище (СУБД)", + "LABEL_ENABLE_CONTACTS": "Увімкнути контакти", + "LABEL_ALLOW_SYNC": "Дозволити синхронізацію контактів (із зовнішнім CardDAV сервером)", + "LABEL_STORAGE_TYPE": "Тип", + "LABEL_STORAGE_DSN": "Dsn", + "LABEL_STORAGE_USER": "Користувач", + "LABEL_STORAGE_PASSWORD": "Пароль", + "SQLITE_GLOBAL": "Використати глобальну БД для всіх, замість окремого для кожного облікового запису", + "ALERT_NOTICE": "Увага!", + "SUGGESTIONS_LIMIT": "Ліміт пропозицій", + "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Не використовуйте цей тип бази даних з великою кількістю активних користувачів.", + "HTML_ALERT_DOES_NOT_SUPPORTED": "Ваша система не підтримує контактів.\n
\nВам необхідно встановити чи увімкнути СУБД (SQLite \/ MySQL \/ PostgreSQL)<\/strong> PHP-розширення на вашому сервері.\n" + }, + "TAB_DOMAINS": { + "LEGEND_DOMAINS": "Домени", + "BUTTON_ADD_DOMAIN": "Додати домен", + "BUTTON_ADD_ALIAS": "Додати аліас", + "DELETE_ARE_YOU_SURE": "Ви впевнені?", + "HTML_DOMAINS_HELPER": "Список доменів до яких дозволено доступ.\n
\nНатисніть на ім'я, щоб настроїти домен.\n" + }, + "TAB_SECURITY": { + "LEGEND_SECURITY": "Безпека", + "LABEL_USE_IMAGE_PROXY": "Використовувати локальний проксі для зовнішніх зображень", + "LABEL_AUTO_VERIFY": "Перевіряти підписи автоматично", + "LABEL_SHOW_PHP_INFO": "Показати PHP інформацію", + "LEGEND_ADMIN_PANEL_ACCESS_CREDENTIALS": "Облікові дані для доступу до панелі адміністратора", + "LABEL_CURRENT_PASSWORD": "Поточний пароль", + "LABEL_NEW_PASSWORD": "Новий пароль", + "LABEL_REPEAT_PASSWORD": "Повторити пароль", + "LABEL_REQUIRE_VERIFICATION": "Вимагати перевірку SSL сертифікату", + "CRYPTO": "Шифрування", + "LABEL_ALLOW_SELF_SIGNED": "Дозволити самопідписані сертифікати" + }, + "TAB_PACKAGES": { + "LABEL_ENABLE_PLUGINS": "Увімкнути плагіни", + "LEGEND_AVAILABLE_FOR_UPDATE": "Є оновлення для наступних плагінів", + "LEGEND_AVAILABLE_FOR_INSTALLATION": "Доступні плагіни", + "LEGEND_INSTALLED_PACKAGES": "Встановлені плагіни", + "ALERT_CANNOT_ACCESS_REPOSITORY": "Наразі не вдається отримати доступ до сховища пакетів." + }, + "TAB_ABOUT": { + "LEGEND_ABOUT": "Про програму", + "LABEL_TAG_HINT": "Простий, сучасний та швидкий веб-клієнт електронної пошти\n", + "LABEL_ALL_RIGHTS_RESERVED": "Всі права захищені.", + "HINT_READ_CHANGE_LOG": "Будь ласка, ознайомтеся зі змінами перед оновленням.\n", + "HINT_IS_UP_TO_DATE": "Версія SnappyMail актуальна.", + "HTML_NEW_VERSION": "Нова %VERSION%<\/b> версія доступна.", + "LABEL_UPDATING": "Оновлення", + "LABEL_CHECKING": "Перевірка", + "BUTTON_RELEASES": "Релізи" + }, + "POPUPS_DOMAIN_ALIAS": { + "TITLE_ADD_DOMAIN_ALIAS": "Додати аліас", + "LABEL_ALIAS": "Аліас", + "LABEL_DOMAIN": "Домен" + }, + "POPUPS_DOMAIN": { + "TITLE_ADD_DOMAIN": "Додати домен", + "TITLE_ADD_DOMAIN_WITH_NAME": "Додати домен \"%NAME%\"", + "TITLE_EDIT_DOMAIN": "Редагувати домен \"%NAME%\"", + "LABEL_NAME": "Імя", + "NAME_HELPER": "підтримуються шаблони", + "LABEL_SERVER": "Сервер", + "LABEL_PORT": "Порт", + "LABEL_SECURE": "Безпека", + "TIMEOUT": "Тайм-аут", + "DISABLE_CAPABILITIES": "Вимкнути функції", + "LIMITS": "Ліміти", + "FOLDERS": "Папки", + "MESSAGES": "Листи", + "SECURE_OPTION_NONE": "Немає", + "LABEL_ALLOW_SIEVE_SCRIPTS": "Дозволити Sieve скрипти", + "LABEL_USE_SHORT_LOGIN": "Використовувати короткий логін", + "LABEL_LOWERCASE_LOGIN": "Логін у нижньому регістрі (незалежно від регістру)", + "LABEL_USE_AUTH": "Використовувати автентифікацію", + "LABEL_SET_SENDER": "Використовувати логін як відправника", + "LABEL_AUTH_PLAIN_LINE": "Примусовий вхід командою AUTH PLAIN", + "LABEL_USE_PHP_MAIL": "Використовувати mail() функцію", + "BUTTON_WHITE_LIST": "Білий список", + "NEW_DOMAIN_DESC": "Ця конфігурація дозволить вам працювати з %NAME%<\/strong> адресами.", + "WHITE_LIST_ALERT": "Список користувачів домену, до яких дозволено доступ.\nВикористовуйте пробіл як роздільник.\n", + "VALID_SPAM_HEADERS": "Valid SPAM headers", + "VALID_VIRUS_HEADERS": "Valid virus headers" + }, + "POPUPS_PLUGIN": { + "TITLE_PLUGIN": "Плагін", + "DESC_NOTHING_TO_CONFIGURE": "Нічого налаштовувати" + }, + "POPUPS_ASK": { + "DESC_WANT_CLOSE_THIS_WINDOW": "Ви впевнені, що хочете закрити це вікно?" + }, + "POPUPS_LANGUAGES": { + "TITLE_LANGUAGES": "Виберіть мову" + }, + "HINTS": { + "BETA": "beta", + "WARNING": "Увага!" + }, + "ERRORS": { + "DOMAIN_ALREADY_EXISTS": "Домен вже існує", + "UNKNOWN_ERROR": "Невідома помилка" + }, + "NOTIFICATIONS": { + "RequestError": "Запит не виконано", + "RequestAborted": "Запит перерваний", + "RequestTimeout": "Минув час запиту", + "InvalidToken": "Невірний токен запиту", + "AuthError": "Не вдалося авторизуватись", + "DomainAlreadyExists": "Домен вже існує", + "CantSavePluginSettings": "Не можу зберегти налаштування", + "CantInstallPackage": "Помилка встановлення плагіна", + "CantDeletePackage": "Помилка видалення плагіна", + "InvalidPluginPackage": "Помилка пакета плагіна", + "UnsupportedPluginPackage": "Для роботи плагіна потрібна повна підтримка сервера", + "UnknownError": "Невідома помилка" + } +} diff --git a/snappymail/v/0.0.0/app/localization/uk/user.json b/snappymail/v/0.0.0/app/localization/uk/user.json index 2e5105f5b..0b35fc3db 100644 --- a/snappymail/v/0.0.0/app/localization/uk/user.json +++ b/snappymail/v/0.0.0/app/localization/uk/user.json @@ -4,14 +4,14 @@ "TO_ARCHIVE": "Архівування", "ARE_YOU_SURE": "Ви впевнені?", "BACK": "Назад", - "BCC": "Прихована", + "BCC": "Прихована копія", "CANCEL": "Відмінити", "CC": "Копія", "CLEAR": "Очистити", "CLOSE": "Закрити (Esc)", "CONTACTS": "Контакти", "DELETE": "Видалити", - "UNDELETE": "Undelete", + "UNDELETE": "Відновити", "DONE": "Зроблено", "EMAIL": "Пошта", "FROM": "Від", @@ -20,26 +20,26 @@ "MORE": "Ще", "MOVE_TO": "Перемістити", "COPY_TO": "Копіювати в", - "NAME": "Вхідні", - "NONE": "None", - "TO_SPAM": "Move message(s) to Spam", + "NAME": "Ім'я", + "NONE": "Нічого", + "TO_SPAM": "Перемістити повідомлення у спам", "NOT_SPAM": "Не спам", "USERNAME": "Ім'я користувача", "PASSWORD": "Пароль", - "REMEMBER": "Remember", - "REPLY_TO": "Reply-To", + "REMEMBER": "Запам'ятати", + "REPLY_TO": "Відповідь сюди", "SAVE": "Зберегти", - "SAVE_CHANGES": "Save changes?", - "SEARCH": "Контакти не знайдено", + "SAVE_CHANGES": "Зберегти зміни?", + "SEARCH": "Пошук", "SPAM": "В спам", "SUBJECT": "Тема", "TEST": "Тест", "TO": "Кому", - "SERVER_MESSAGE": "Server message", + "SERVER_MESSAGE": "Повідомлення сервера", "YES": "Так", "NO": "Ні", - "INACTIVE": "Inactive", - "DEFAULT": "default", + "INACTIVE": "Не активне", + "DEFAULT": "за замовчуванням", "HELP": "Допомога", "TEXT": "Текст", "ATTACHMENTS": "Файли" @@ -56,51 +56,51 @@ "TITLE_ADV": "Розширений пошук листів", "TEXT": "Текст", "HAS_ATTACHMENT": "З файлами", - "FLAGGED": "Відмічені", + "FLAGGED": "Із зірочкою", "UNSEEN": "Непрочитані", "DATE": "Дата", "DATE_ALL": "За весь час", - "BEFORE_3_DAYS": "Older then 3 days", - "BEFORE_7_DAYS": "Older then 1 week", - "BEFORE_MONTH": "Older then 1 month", - "BEFORE_3_MONTHS": "Older then 3 months", - "BEFORE_6_MONTHS": "Older then 6 months", - "BEFORE_YEAR": "Older then 1 year", + "BEFORE_3_DAYS": "Старше 3 днів", + "BEFORE_7_DAYS": "Старше 1 тижня", + "BEFORE_MONTH": "Старше 1 місяця", + "BEFORE_3_MONTHS": "Старше 3 місяців", + "BEFORE_6_MONTHS": "Старше 6 місяців", + "BEFORE_YEAR": "Старше 1 року", "SINCE_3_DAYS": "За три дні", "SINCE_7_DAYS": "За тиждень", "SINCE_MONTH": "За місяць", "SINCE_3_MONTHS": "За 3 місяці", "SINCE_6_MONTHS": "За півроку", "SINCE_YEAR": "За рік", - "SUBFOLDERS": "Subfolders", - "SUBFOLDERS_NONE": "None", - "SUBFOLDERS_SUBTREE": "All", - "SUBFOLDERS_SUBTREE_ONE": "One level", - "REPLIED": "відповів", - "KEYWORD": "Tag" + "SUBFOLDERS": "Підпапки", + "SUBFOLDERS_NONE": "Нічого", + "SUBFOLDERS_SUBTREE": "Усі", + "SUBFOLDERS_SUBTREE_ONE": "Один рівень", + "REPLIED": "Відповів", + "KEYWORD": "Тег" }, "FOLDER_LIST": { "BUTTON_COMPOSE": "Написати", - "BUTTON_NEW_MESSAGE": "New message", + "BUTTON_NEW_MESSAGE": "Новий лист", "INBOX_NAME": "Вхідні", "SENT_NAME": "Вихідні", - "DRAFTS_NAME": "Чорновики", + "DRAFTS_NAME": "Чернетки", "TRASH_NAME": "Кошик", "ARCHIVE_NAME": "Архів" }, "IMAP": { - "ACL_RIGHTS": "Rights", - "ACL_A": "Administer", - "ACL_E": "Expunge", - "ACL_I": "Insert messages", - "ACL_K": "Create sub-folders", - "ACL_L": "Show messages", - "ACL_P": "Send mail", - "ACL_R": "Select folder", - "ACL_S": "\\SEEN tag", - "ACL_T": "\\DELETED tag", - "ACL_W": "Other tags", - "ACL_X": "Delete\/Rename folder" + "ACL_RIGHTS": "Права", + "ACL_A": "Адмініструвати", + "ACL_E": "Видалити", + "ACL_I": "Вставте повідомлення", + "ACL_K": "Створити вкладені папки", + "ACL_L": "Показати повідомлення", + "ACL_P": "Надіслати лист", + "ACL_R": "Виберіть папку", + "ACL_S": "\\ПЕРЕГЛЯНУТИ тег", + "ACL_T": "\\ВИДАЛЕНО тег", + "ACL_W": "Інші теги", + "ACL_X": "Видалити\/Перейменувати папку" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Оновити список листів", @@ -110,15 +110,15 @@ "MENU_SET_SEEN": "Відмітити як прочитане", "MENU_SET_ALL_SEEN": "Відмітити всі як прочитані", "MENU_UNSET_SEEN": "Відмітити як непрочитане", - "MENU_SET_FLAG": "Встановити флаг", - "MENU_UNSET_FLAG": "Зняти флаг", + "MENU_SET_FLAG": "Встановити зірочку", + "MENU_UNSET_FLAG": "Зняти зірочку", "MENU_SELECT_ALL": "Все", "MENU_SELECT_INVERT": "Інвертувати", "MENU_SELECT_UNSEEN": "Непрочитані", "MENU_SELECT_SEEN": "Прочитані", - "MENU_SELECT_FLAGGED": "Відмічені", - "MENU_SELECT_UNFLAGGED": "Без поміток", - "DOWNLOAD_ALL_ATTACHMENTS": "Download all attachments", + "MENU_SELECT_FLAGGED": "Із Зірочкою", + "MENU_SELECT_UNFLAGGED": "Без зірочки", + "DOWNLOAD_ALL_ATTACHMENTS": "Завантажити всі вкладення", "EMPTY_LIST": "В папці немає листів.", "EMPTY_SEARCH_LIST": "Листи не знайдено.", "SEARCH_RESULT_FOR": "Результат пошуку \"%SEARCH%\"", @@ -128,20 +128,22 @@ "YESTERDAY_AT": "вчора в %TIME%", "NEW_MESSAGE_NOTIFICATION": "У вас %COUNT% нових повідомлень!", "QUOTA_SIZE": "Використано %SIZE% (%PROC%%)<\/strong> з %LIMIT%<\/strong>", - "SORT": "Sort", - "NO_SORT": "Mail server does not support sorting", - "SORT_DATE_ASC": "Date ascending", - "SORT_DATE_DESC": "Date descending", - "SORT_SIZE_ASC": "Size ascending", - "SORT_SIZE_DESC": "Size descending", - "SORT_SUBJECT_ASC": "Subject A-Z", - "SORT_SUBJECT_DESC": "Subject Z-A", - "SORT_FROM_ASC": "From A-Z", - "SORT_FROM_DESC": "From Z-A" + "SORT": "Сортувати", + "NO_SORT": "Поштовий сервер не підтримує сортування", + "SORT_ARRIVAL_ASC": "Доставлено спершу старі", + "SORT_ARRIVAL_DESC": "Доставлено спершу нові", + "SORT_DATE_ASC": "Дата спершу старі", + "SORT_DATE_DESC": "Дата спершу нові", + "SORT_SIZE_ASC": "Розмір за зростанням", + "SORT_SIZE_DESC": "Розмір за спаданням", + "SORT_SUBJECT_ASC": "Тема А-Я", + "SORT_SUBJECT_DESC": "Тема Я-А", + "SORT_FROM_ASC": "Відправник А-Я", + "SORT_FROM_DESC": "Відправник Я-A" }, "MESSAGE": { "BUTTON_EDIT": "Редагувати", - "BUTTON_UNSUBSCRIBE": "Unsubscribe from this list", + "BUTTON_UNSUBSCRIBE": "Відписатися", "BUTTON_REPLY": "Відповісти на Листа", "BUTTON_REPLY_ALL": "Відповісти Всім", "BUTTON_FORWARD": "Переслати", @@ -156,9 +158,9 @@ "BUTTON_THREAD_MORE": "ще листи", "MENU_HEADERS": "Подивитися заголовки", "MENU_VIEW_ORIGINAL": "Подивитися оригінал", - "HTML_VIEW": "View HTML message", - "PLAIN_VIEW": "View plain text message", - "SWAP_COLORS": "Swap default (background)color", + "HTML_VIEW": "Переглянути HTML версію", + "PLAIN_VIEW": "Переглянути текстову версію", + "SWAP_COLORS": "Змінити стандартний (фоновий) колір", "MENU_DOWNLOAD_ORIGINAL": "Завантажити як .eml файл", "MENU_FILTER_SIMILAR": "Фільтрувати схожі", "MENU_PRINT": "Надрукувати", @@ -168,23 +170,23 @@ "LABEL_TO_SHORT": "кому", "PRINT_LABEL_ATTACHMENTS": "Файли", "MESSAGE_VIEW_DESC": "Виберіть повідомлення для перегляду.", - "MESSAGE_VIEW_MOVE_DESC": "Click folder name in the left panel to select the destination.", + "MESSAGE_VIEW_MOVE_DESC": "Клікніть на папку на лівій панелі, щоб вибрати місце призначення.", "LINK_DOWNLOAD_AS_ZIP": "Завантажити як zip", - "SPAM_SCORE": "Spam Score", - "HAS_VIRUS_WARNING": "WARNING: virus detected", - "TAGS": "Tags", - "NEW_TAG": "New tag", - "ENABLE_TRACKING_LINKS": "Enable tracking links", - "TRACKING_ENABLED": "Links are now tracked when clicked!" + "SPAM_SCORE": "Оцінка спаму", + "HAS_VIRUS_WARNING": "ПОПЕРЕДЖЕННЯ: виявлено вірус", + "TAGS": "Теги", + "NEW_TAG": "Новий тег", + "ENABLE_TRACKING_LINKS": "Увімкнути відстеження посилань", + "TRACKING_ENABLED": "Посилання тепер відстежуються при натисканні!" }, "MESSAGE_TAGS": { - "$important": "Important", - "$todo": "To Do", - "$label1": "Important", - "$label2": "Work", - "$label3": "Personal", - "$label4": "To Do", - "$label5": "Later" + "$important": "Важливе", + "$todo": "Зробити", + "$label1": "Важливе", + "$label2": "Робота", + "$label3": "Особисте", + "$label4": "Зробити", + "$label5": "Пізніше" }, "READ_RECEIPT": { "SUBJECT": "Сповіщення про прочитання цього повідомлення - %SUBJECT%", @@ -194,17 +196,17 @@ "SEARCHING_DESC": "Пошук..." }, "CONTACTS": { - "TAB_CONTACT": "Contact", - "TAB_LOCATIONS": "Locations", - "TAB_BUSINESS": "Business", - "LABEL_ORGANIZATION": "Organization", - "LABEL_DEPARTMENT": "Department", - "LABEL_TITLE": "Title", - "TAB_CRYPTO": "Crypto", - "ASK": "Ask", - "NEVER": "Never", - "ALWAYS": "Always", - "ALWAYS_IF_POSSIBLE": "Always if possible", + "TAB_CONTACT": "Контакт", + "TAB_LOCATIONS": "Розташування", + "TAB_BUSINESS": "Організація", + "LABEL_ORGANIZATION": "Компанія", + "LABEL_DEPARTMENT": "Департамент", + "LABEL_TITLE": "Назва", + "TAB_CRYPTO": "Шифрування", + "ASK": "Запитувати", + "NEVER": "Ніколи", + "ALWAYS": "Завжди", + "ALWAYS_IF_POSSIBLE": "Завжди якщо можливо", "BUTTON_ADD_CONTACT": "Додати контакт", "BUTTON_CREATE_CONTACT": "Зберегти", "BUTTON_IMPORT": "Імпорт (csv, vcf, vCard)", @@ -219,9 +221,9 @@ "DISPLAY_NAME": "Повне ім'я", "LAST_NAME": "Ім'я", "FIRST_NAME": "Прізвище", - "MIDDLE_NAME": "Middle name", - "NAME_PREFIX": "Name prefix", - "NAME_SUFFIX": "Name suffix", + "MIDDLE_NAME": "По батькові", + "NAME_PREFIX": "Префікс імені", + "NAME_SUFFIX": "Суфікс імені", "NICK_NAME": "Нік", "LABEL_READ_ONLY": "Тільки читання", "ADD_MENU_LABEL": "Додати", @@ -234,34 +236,34 @@ "ADD_MENU_TAGS": "Теги", "BUTTON_SHARE_ALL": "Всім", "BUTTON_SYNC": "Синхронизація (CardDAV)", - "SEND_TO_ALL_CONTACT_EMAILS": "Send to all contact emails" + "SEND_TO_ALL_CONTACT_EMAILS": "Надіслати на всі контактні email" }, "COMPOSE": { "LINK_SHOW_INPUTS": "Показати всі поля", "BUTTON_SEND": "Надіслати", - "BUTTON_MINIMIZE": "Minimize", + "BUTTON_MINIMIZE": "Згорнути", "SAVED_TIME": "Збережено в %TIME%", "SAVED_ERROR_ON_SEND": "Повідомлення було надіслано, але не збережено", - "DISCARD_UNSAVED_DATA": "Discard unsaved data?", + "DISCARD_UNSAVED_DATA": "Видалити незбережені дані?", "ATTACH_FILES": "Додати файли", "ATTACH_DROP_FILES_DESC": "Помістити файли сюди", "REPLY_MESSAGE_TITLE": "%DATETIME%, %EMAIL% написав", "FORWARD_MESSAGE_TOP_TITLE": "-------- Переслане повідомлення -------", "FORWARD_MESSAGE_TOP_SENT": "Надіслано", "EMPTY_TO_ERROR_DESC": "Вкажіть як мінімум одного отримувача", - "NO_ATTACHMENTS_HERE_DESC": "No attachments here.", - "ATTACHMENTS_ERROR_DESC": "Warning! Not all attachments have been uploaded.", - "ATTACHMENTS_UPLOAD_ERROR_DESC": "Not all attachments have been uploaded yet", - "ALLOW_SPELLCHECK": "Enable browser spelling checker", - "ALLOW_DRAFT_AUTOSAVE": "Automatically save draft", + "NO_ATTACHMENTS_HERE_DESC": "Тут немає вкладень.", + "ATTACHMENTS_ERROR_DESC": "УВАГА! Не всі вкладення завантажено.", + "ATTACHMENTS_UPLOAD_ERROR_DESC": "Ще не всі вкладені файли завантажено", + "ALLOW_SPELLCHECK": "Увімкнути перевірку правопису в браузері", + "ALLOW_DRAFT_AUTOSAVE": "Автоматично зберігати чернетку", "BUTTON_REQUEST_READ_RECEIPT": "Запит про прочитання листа", - "BUTTON_MARK_AS_IMPORTANT": "Mark as important", - "BUTTON_REQUEST_DSN": "Request a delivery receipt", - "BUTTON_REQUIRE_TLS": "Require secure delivery", - "ERROR_EMPTY_BODY": "Message body is empty" + "BUTTON_MARK_AS_IMPORTANT": "Позначити як важливе", + "BUTTON_REQUEST_DSN": "Запитати звіт про доставку", + "BUTTON_REQUIRE_TLS": "Вимагати захищеної доставки", + "ERROR_EMPTY_BODY": "Повідомлення порожнє" }, "POPUPS_ASK": { - "EXIT_ARE_YOU_SURE": "Are you sure you want to exit?", + "EXIT_ARE_YOU_SURE": "Ви впевнені, що бажаєте вийти?", "DESC_WANT_CLOSE_THIS_WINDOW": "Ви впевнені, що хочете закрити це вікно?", "DESC_WANT_DELETE_MESSAGES": "Ви впевнені, що хочете видалити повідомлення?" }, @@ -271,43 +273,43 @@ "POPUPS_ADD_ACCOUNT": { "TITLE_ADD_ACCOUNT": "Додати ще акаунт", "BUTTON_ADD_ACCOUNT": "Додати", - "TITLE_UPDATE_ACCOUNT": "Update Account" + "TITLE_UPDATE_ACCOUNT": "Оновити акаунт" }, "POPUPS_IDENTITY": { - "TITLE_ADD_IDENTITY": "Додати Профіль", - "TITLE_UPDATE_IDENTITY": "Оновити Профіль", - "LABEL": "Label", + "TITLE_ADD_IDENTITY": "Додати відправника", + "TITLE_UPDATE_IDENTITY": "Оновити відправника", + "LABEL": "Внутрішня назва", "BUTTON_ADD_IDENTITY": "Додати", - "SIGNATURE": "Signature", - "LABEL_SIGNATURE_INSERT_BEFORE": "Insert this signature before quoted text in replies" + "SIGNATURE": "Підпис", + "LABEL_SIGNATURE_INSERT_BEFORE": "Вставляти підпис перед цитованим текстом у відповідях" }, "POPUPS_CREATE_FOLDER": { - "TITLE_CREATE_FOLDER": "Створити теку", - "LABEL_PARENT": "Батьківська тека", + "TITLE_CREATE_FOLDER": "Створити папку", + "LABEL_PARENT": "Батьківська папка", "BUTTON_CREATE": "Створити", - "TITLE_CREATING_PROCESS": "Створення теки" + "TITLE_CREATING_PROCESS": "Створення папки" }, "POPUPS_CLEAR_FOLDER": { - "TITLE_CLEAR_FOLDER": "Очистити теку від листів?", + "TITLE_CLEAR_FOLDER": "Очистити папку від листів?", "DANGER_DESC_WARNING": "Увага!", - "DANGER_DESC_HTML_1": "Ця дія призведе до повного видалення всіх листів з теки %FOLDER%<\/strong>.", + "DANGER_DESC_HTML_1": "Ця дія призведе до повного видалення всіх листів з папки %FOLDER%<\/strong>.", "DANGER_DESC_HTML_2": "Після початку очищення, процес неможливо<\/strong> буде зупинити чи відмінити.", - "TITLE_CLEARING_PROCESS": "Очищення теки..." + "TITLE_CLEARING_PROCESS": "Очищення папки..." }, "CRYPTO": { "SIGN": "Підпис", "ENCRYPT": "Шифрування", "DECRYPT": "Дешифрувати", - "VERIFY": "Verify", + "VERIFY": "Перевірити", "GOOD_SIGNATURE": "Підтверджена сигнатура для %USER%", "ERROR": "%TYPE% помилка: %ERROR%", - "CREATE_SELF_SIGNED": "Create self-signed", - "VALID_UNTIL": "Valid until", + "CREATE_SELF_SIGNED": "Створити самопідписаний", + "VALID_UNTIL": "Дійсний до", "PRIVATE_KEY": "Private key", - "CHANGE_PASS": "Change passphrase", - "CURRENT_PASS": "Current passphrase", - "NEW_PASS": "New passphrase", - "ASK_CRYPTKEY_PASS": "Your login passphrase has changed. Please fill in your old login passphrase to decrypt data" + "CHANGE_PASS": "Змінити пароль", + "CURRENT_PASS": "Поточна пароль", + "NEW_PASS": "Новий пароль", + "ASK_CRYPTKEY_PASS": "Ваш пароль змінено. Щоб розшифрувати дані, введіть старий пароль" }, "OPENPGP": { "POPUP_IMPORT_TITLE": "Імпорт OpenPGP ключа", @@ -319,20 +321,20 @@ "LABEL_KEY_TYPE": "Type", "SIGNED_MESSAGE": "OpenPGP підписане повідомлення", "ENCRYPTED_MESSAGE": "OpenPGP шифроване повідомлення", - "STORE_IN_GNUPG": "Store on server in GnuPG", - "STORE_PUBLIC_KEY_IN_GNUPG": "Store public key on server in GnuPG", - "STORE_PRIVATE_KEY_IN_GNUPG": "Store private key on server in GnuPG", - "BACKUP_ON_SERVER": "Store (encrypted) on server", - "BACKUP_PUBLIC_KEY_ON_SERVER": "Backup public key on server", - "BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server" + "STORE_IN_GNUPG": "Зберігати на сервері в GnuPG", + "STORE_PUBLIC_KEY_IN_GNUPG": "Зберігайте public key на сервері в GnuPG", + "STORE_PRIVATE_KEY_IN_GNUPG": "Зберігайте private key на сервері в GnuPG", + "BACKUP_ON_SERVER": "Зберігати (зашифроване) на сервері", + "BACKUP_PUBLIC_KEY_ON_SERVER": "Бекап public key на сервері", + "BACKUP_PRIVATE_KEY_ON_SERVER": "Бекап private key на сервері" }, "SMIME": { - "POPUP_IMPORT_TITLE": "Import S\/MIME certificate", - "CERTIFICATE": "Certificate", - "CERTIFICATES": "S\/MIME Certificates", + "POPUP_IMPORT_TITLE": "Імпорт S\/MIME сертифікат", + "CERTIFICATE": "Сертифікат", + "CERTIFICATES": "S\/MIME Сертифікати", "SIGNED_MESSAGE": "S\/MIME підписане повідомлення", "ENCRYPTED_MESSAGE": "S\/MIME шифроване повідомлення", - "PRIVATE_KEY_OF": "S\/MIME private key of %EMAIL%" + "PRIVATE_KEY_OF": "S\/MIME private key з %EMAIL%" }, "POPUPS_FILTER": { "TITLE_CREATE_FILTER": "Створити фільтер", @@ -347,26 +349,26 @@ "SELECT_ACTION_DISCARD": "Відклонити", "SELECT_FIELD_RECIPIENTS": "Отримувачі (To or CC)", "SELECT_FIELD_HEADER": "Заголовок", - "SELECT_FIELD_BODY": "Body", + "SELECT_FIELD_BODY": "Тіло", "SELECT_FIELD_SIZE": "Розмір", "SELECT_TYPE_CONTAINS": "Містить", "SELECT_TYPE_NOT_CONTAINS": "Не містить", - "SELECT_TYPE_MATCHES": "Matches (* and ? supported)", - "SELECT_TYPE_NOT_MATCHES": "Not Matches (* and ? supported)", - "SELECT_TYPE_REGEXP": "Regexp", - "SELECT_TYPE_NOT_REGEXP": "Not Regexp", + "SELECT_TYPE_MATCHES": "Збіги (* та ? підтримуються)", + "SELECT_TYPE_NOT_MATCHES": "Заперечення збігів (* та ? підтримуються)", + "SELECT_TYPE_REGEXP": "Регулярний вираз", + "SELECT_TYPE_NOT_REGEXP": "Не регулярний вираз", "SELECT_TYPE_EQUAL_TO": "Дорівнює", "SELECT_TYPE_NOT_EQUAL_TO": "Не дорівнює", - "SELECT_TYPE_OVER": "Over", - "SELECT_TYPE_UNDER": "Under", - "SELECT_TYPE_TEXT": "Text", + "SELECT_TYPE_OVER": "Більше", + "SELECT_TYPE_UNDER": "Менше", + "SELECT_TYPE_TEXT": "Текст", "SELECT_TYPE_RAW": "Raw", - "SELECT_MATCH_ANY": "Matching any of the following rules", - "SELECT_MATCH_ALL": "Matching all of the following rules", - "MARK_AS_READ_LABEL": "Mark as read", - "REPLY_INTERVAL_LABEL": "Reply interval (days)", - "KEEP_LABEL": "Keep", - "STOP_LABEL": "Stop further processing of rules", + "SELECT_MATCH_ANY": "Відповідає будь-якому з наступних правил", + "SELECT_MATCH_ALL": "Відповідає усім наступним правилам", + "MARK_AS_READ_LABEL": "Позначити як прочитане", + "REPLY_INTERVAL_LABEL": "Інтервал відповідей (днів)", + "KEEP_LABEL": "Тримати", + "STOP_LABEL": "Зупинити подальшу обробку правил", "VACATION_SUBJECT_LABEL": "Тема (не обов'язково)", "VACATION_MESSAGE_LABEL": "Повідомлення", "VACATION_RECIPIENTS_LABEL": "Отримувачі (розділяти комами)", @@ -374,22 +376,22 @@ "ALL_INCOMING_MESSAGES_DESC": "Всі повідомлення, що надійшли" }, "POPUPS_SIEVE_SCRIPT": { - "TITLE_CREATE": "Create Script", - "TITLE_EDIT": "Edit Script", - "BUTTON_ADD_FILTER": "Add a Filter", - "BUTTON_RAW_SCRIPT": "Use Custom User Script", - "CAPABILITY_LABEL": "Capabilities", - "CHANGES_NEED_TO_BE_SAVED_DESC": "These changes need to be saved to the server." + "TITLE_CREATE": "Створити скрипт", + "TITLE_EDIT": "Редагувати скрипт", + "BUTTON_ADD_FILTER": "Додати фільтр", + "BUTTON_RAW_SCRIPT": "Використати користувацький скрипт", + "CAPABILITY_LABEL": "Можливості", + "CHANGES_NEED_TO_BE_SAVED_DESC": "Ці зміни потрібно зберегти на сервері." }, "POPUPS_SYSTEM_FOLDERS": { - "TITLE_SYSTEM_FOLDERS": "Виберіть системні теки", + "TITLE_SYSTEM_FOLDERS": "Виберіть системні папки", "SELECT_CHOOSE_ONE": "Ще не вибрано", "SELECT_UNUSE_NAME": "Не використовувати", - "NOTIFICATION_SENT": "Ви ще не обрали системну теку \"Вихідні\", в яку складаються всі повідомлення післе надсилання.\nЯкщо ви не хочете зберігати вихідні листи, будь ласка, виберіть пункт \"Не використовувати\".\n", - "NOTIFICATION_DRAFTS": "Ви ще не обрали системну теку \"Чорновики\", в яку складаються всі збережені повідомлення.", - "NOTIFICATION_SPAM": "Ви ще не обрали системну теку \"Спам\", в яку складаються всі спам повідомлення.\nЯкщо ви не хочете видаляти листи одразу, будь ласка, виберіть пункт \"Не використовувати\".\n", - "NOTIFICATION_TRASH": "Ви ще не обрали системну теку \"Удаленные\", в яку складаються всі повідомлення після видалення.\nЯкщо ви не хочете видаляти листи одразу, будь ласка, виберіть пункт \"Не використовувати\".\n", - "NOTIFICATION_ARCHIVE": "Ви ще не обрали системну теку \"Архив\", в яку складаються всі повідомлення після архівації." + "NOTIFICATION_SENT": "Ви ще не обрали системну папку \"Вихідні\", в яку складаються всі повідомлення післе надсилання.\nЯкщо ви не хочете зберігати вихідні листи, будь ласка, виберіть пункт \"Не використовувати\".\n", + "NOTIFICATION_DRAFTS": "Ви ще не обрали системну папку \"Чорновики\", в яку складаються всі збережені повідомлення.", + "NOTIFICATION_SPAM": "Ви ще не обрали системну папку \"Спам\", в яку складаються всі спам повідомлення.\nЯкщо ви не хочете видаляти листи одразу, будь ласка, виберіть пункт \"Не використовувати\".\n", + "NOTIFICATION_TRASH": "Ви ще не обрали системну папку \"Удаленные\", в яку складаються всі повідомлення після видалення.\nЯкщо ви не хочете видаляти листи одразу, будь ласка, виберіть пункт \"Не використовувати\".\n", + "NOTIFICATION_ARCHIVE": "Ви ще не обрали системну папку \"Архив\", в яку складаються всі повідомлення після архівації." }, "TITLES": { "MAILBOX": "Пошта", @@ -405,68 +407,68 @@ "ERROR_UNKNOWN": "Помилка завантаження файлу" }, "EDITOR": { - "PLAIN": "HTML <-> TEXT", - "SOURCE": "HTML <-> SOURCE", - "FONTFAMILY": "Font family", - "FONTSIZE": "Font size", - "DIR": "Text direction", - "DIR_LTR": "Left to right", - "DIR_RTL": "Right to left", - "DIR_AUTO": "Automatic", - "TEXTCOLOR": "Text color", - "BACKGROUNDCOLOR": "Background color", - "BOLD": "Bold", - "ITALIC": "Italic", - "UNDERLINE": "Underline", - "STRIKE": "Strikethrough", - "SUB": "Subscript", - "SUP": "Superscript", - "OL": "Ordered list", - "UL": "Unordered list", - "QUOTE": "Blockquote", - "INDENTDECREASE": "Decrease indent", - "INDENTINCREASE": "Increase indent", - "LINK": "Link", - "IMAGEURL": "Image URL", - "IMAGEUPLOAD": "Image select", - "UNDO": "Undo", - "REDO": "Redo", - "REMOVESTYLE": "Remove style" + "PLAIN": "HTML <-> ТЕКСТ", + "SOURCE": "HTML <-> ДЖЕРЕЛО", + "FONTFAMILY": "Сімейство шрифтів", + "FONTSIZE": "Розмір шрифту", + "DIR": "Напрямок тексту", + "DIR_LTR": "Зліва направо", + "DIR_RTL": "Справа наліво", + "DIR_AUTO": "Автоматично", + "TEXTCOLOR": "Колір тексту", + "BACKGROUNDCOLOR": "Колір фону", + "BOLD": "Жирний", + "ITALIC": "Курсив", + "UNDERLINE": "Підкреслений", + "STRIKE": "Закреслений", + "SUB": "Нижній індекс", + "SUP": "Верхній індекс", + "OL": "Нумерований список", + "UL": "Маркований список", + "QUOTE": "Блок цитати", + "INDENTDECREASE": "Зменшити відступ", + "INDENTINCREASE": "Збільшити відступ", + "LINK": "Посилання", + "IMAGEURL": "URL зображення", + "IMAGEUPLOAD": "Вибір зображення", + "UNDO": "Скасувати", + "REDO": "Повторити", + "REMOVESTYLE": "Видалити стиль" }, "SETTINGS_LABELS": { "GENERAL": "Основні", "CONTACTS": "Контакти", - "FOLDERS": "Теки", + "FOLDERS": "Папки", "ACCOUNTS": "Акаунти", - "FILTERS": "Фильтры", + "FILTERS": "Фільтри", "SECURITY": "Безпека", "THEMES": "Теми" }, "SETTINGS_FILTERS": { - "SIMPLE": "Simple", - "ADVANCED": "Advanced", - "BUTTON_ADD_SCRIPT": "Add a Script", - "SUBNAME_MOVE_TO": "Move to \"%FOLDER%\"", - "SUBNAME_FORWARD_TO": "Forward to \"%EMAIL%\"", - "SUBNAME_REJECT": "Reject", - "SUBNAME_VACATION_MESSAGE": "Повідомлення про відпустку", - "SUBNAME_DISCARD": "Відклонити" + "SIMPLE": "Прості", + "ADVANCED": "Розширені", + "BUTTON_ADD_SCRIPT": "Додати скрипт", + "SUBNAME_MOVE_TO": "Перемістити до \"%FOLDER%\"", + "SUBNAME_FORWARD_TO": "Переслати на \"%EMAIL%\"", + "SUBNAME_REJECT": "Відхилити", + "SUBNAME_VACATION_MESSAGE": "Повідомлення автовідповідача", + "SUBNAME_DISCARD": "Знищити" }, "SETTINGS_SECURITY": { "LEGEND_SECURITY": "Безпека", "LABEL_AUTOLOGOUT": "Автовихід", - "FORGET_KEY_PASS": "Forget private key passphrase", + "FORGET_KEY_PASS": "Забувати пароль private key", "NEVER": "Ніколи" }, "SETTINGS_GENERAL": { "LANGUAGE": "Мова", - "HOUR_FORMAT": "Hour format", - "IDENTITY": "Identity", - "LAYOUT": "Layout", - "LAYOUT_NO_SPLIT": "No Split", - "LAYOUT_VERTICAL_SPLIT": "Vertical Split", - "LAYOUT_HORIZONTAL_SPLIT": "Horizontal Split", - "SHOW_NEXT_MESSAGE": "Show next message when (re)move current message", + "HOUR_FORMAT": "Формат часу", + "IDENTITY": "Акаунт", + "LAYOUT": "Макет", + "LAYOUT_NO_SPLIT": "Список листів", + "LAYOUT_VERTICAL_SPLIT": "Список з вертикальним переглядом", + "LAYOUT_HORIZONTAL_SPLIT": "Список з горизонтальним переглядом", + "SHOW_NEXT_MESSAGE": "Перейти до наступного повідомлення після видалення\/переміщення поточного", "EDITOR": "Редактор тексту за змовчуванням", "EDITOR_HTML": "Форматований", "EDITOR_PLAIN": "Простий", @@ -475,35 +477,35 @@ "HIDE_DELETED_MESSAGES": "Приховати повідомлення, позначені для видалення", "USE_CHECKBOXES_IN_LIST": "Показувати чекбокси у списках", "USE_THREADS": "Згортати листи у листуваннях", - "THREAD_ALGORITHM": "Threads algorithm", - "REPLY_SAME_FOLDER": "Поміщати відповіді в ту ж теку, де знаходиться оригінал", - "PREFER_HTML": "Prefer HTML to plain text", - "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", - "REMOVE_COLORS": "Remove background and text colors from message body", - "ALLOW_STYLES": "Allow