diff --git a/.docker/release/files/usr/local/include/application.ini b/.docker/release/files/usr/local/include/application.ini index 6d8745dce..9f0a6b09e 100644 --- a/.docker/release/files/usr/local/include/application.ini +++ b/.docker/release/files/usr/local/include/application.ini @@ -259,8 +259,6 @@ use_imap_force_selection = Off use_imap_thread = On use_imap_move = Off use_imap_expunge_all_on_delete = Off -imap_forwarded_flag = "$Forwarded" -imap_read_receipt_flag = "$ReadReceipt" imap_body_text_limit = 555000 imap_message_list_fast_simple_search = On imap_message_list_count_limit_trigger = 0 @@ -307,5 +305,5 @@ dev_email = "" dev_password = "" [version] -current = "2.16.1" +current = "2.16.3" saved = "Fri, 04 Mar 2022 08:55:26 +0000" diff --git a/README.md b/README.md index 376cc8db7..75341a93d 100644 --- a/README.md +++ b/README.md @@ -140,26 +140,26 @@ RainLoop 1.15 vs SnappyMail |js/* |RainLoop |Snappy | |--------------- |--------: |--------: | -|admin.js |2.158.025 | 80.335 | -|app.js |4.215.733 | 405.051 | +|admin.js |2.158.025 | 80.521 | +|app.js |4.215.733 | 405.835 | |boot.js | 672.433 | 2.050 | |libs.js | 647.679 | 197.160 | -|sieve.js | 0 | 86.504 | +|sieve.js | 0 | 86.362 | |polyfills.js | 325.908 | 0 | |serviceworker.js | 0 | 285 | -|TOTAL |8.019.778 | 771.385 | +|TOTAL |8.019.778 | 772.213 | |js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |--------------- |--------: |--------: |------: |------: |--------: |--------: | -|admin.min.js | 255.514 | 40.186 | 73.899 | 13.359 | 60.674 | 11.966 | -|app.min.js | 516.000 | 191.792 |140.430 | 61.896 |110.657 | 53.154 | +|admin.min.js | 255.514 | 40.289 | 73.899 | 13.383 | 60.674 | 11.971 | +|app.min.js | 516.000 | 192.189 |140.430 | 62.006 |110.657 | 53.281 | |boot.min.js | 66.456 | 1.252 | 22.553 | 778 | 20.043 | 628 | |libs.min.js | 574.626 | 95.110 |177.280 | 35.149 |151.855 | 31.461 | -|sieve.min.js | 0 | 42.147 | 0 | 10.551 | 0 | 9.513 | +|sieve.min.js | 0 | 42.091 | 0 | 10.543 | 0 | 9.511 | |polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 | -|TOTAL user |1.189.690 | 288.154 |351.061 | 97.823 |292.627 | 85.243 | -|TOTAL user+sieve |1.189.690 | 330.301 |351.061 |108.374 |292.627 | 94.756 | -|TOTAL admin | 929.204 | 136.548 |285.047 | 49.286 |242.644 | 44.055 | +|TOTAL user |1.189.690 | 288.551 |351.061 | 97.933 |292.627 | 85.370 | +|TOTAL user+sieve |1.189.690 | 330.642 |351.061 |108.476 |292.627 | 94.881 | +|TOTAL admin | 929.204 | 136.651 |285.047 | 49.310 |242.644 | 44.060 | For a user its around 70% smaller and faster than traditional RainLoop. @@ -187,8 +187,8 @@ For a user its around 70% smaller and faster than traditional RainLoop. |css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli | |------------ |-------: |------: |------: |------: |--------: | -|app.css | 340.334 | 80.643 | 46.959 | 16.709 | 14.386 | -|app.min.css | 274.791 | 64.822 | 39.618 | 14.793 | 13.044 | +|app.css | 340.334 | 80.979 | 46.959 | 16.774 | 14.439 | +|app.min.css | 274.791 | 65.092 | 39.618 | 14.841 | 13.085 | |boot.css | | 1.326 | | 664 | 545 | |boot.min.css | | 1.071 | | 590 | 474 | |admin.css | | 29.828 | | 6.777 | 5.875 | diff --git a/dev/Model/Message.js b/dev/Model/Message.js index 276adf7a8..ab46130ac 100644 --- a/dev/Model/Message.js +++ b/dev/Model/Message.js @@ -97,11 +97,8 @@ export class MessageModel extends AbstractModel { isImportant: () => MessagePriority.High === this.priority(), hasAttachments: () => this.attachments().hasVisible(), - isDeleted: () => this.flags().includes('\\deleted'), - isUnseen: () => !this.flags().includes('\\seen') /* || this.flags().includes('\\unseen')*/, + isUnseen: () => !this.flags().includes('\\seen'), isFlagged: () => this.flags().includes('\\flagged'), - isAnswered: () => this.flags().includes('\\answered'), - isForwarded: () => this.flags().includes('$forwarded'), isReadReceipt: () => this.flags().includes('$mdnsent') // isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'), // isPhishing: () => this.flags().includes('$phishing') @@ -291,13 +288,9 @@ export class MessageModel extends AbstractModel { let classes = []; forEachObjectEntry({ deleted: this.deleted(), - 'deleted-mark': this.isDeleted(), selected: this.selected(), checked: this.checked(), - flagged: this.isFlagged(), unseen: this.isUnseen(), - answered: this.isAnswered(), - forwarded: this.isForwarded(), focused: this.focused(), important: this.isImportant(), withAttachments: !!this.attachments().length, @@ -306,9 +299,18 @@ export class MessageModel extends AbstractModel { hasUnseenSubMessage: this.hasUnseenSubMessage(), hasFlaggedSubMessage: this.hasFlaggedSubMessage() }, (key, value) => value && classes.push(key)); + this.flags().forEach(value => classes.push('flag-'+value)); return classes.join(' '); } + /** + * @return array + * https://datatracker.ietf.org/doc/html/rfc5788 + */ + keywords() { + return this.flags().filter(value => '\\' !== value[0]); + } + /** * @returns {string} */ @@ -567,19 +569,4 @@ export class MessageModel extends AbstractModel { return result.html || plainToHtml(this.plain()); } - /** - * @returns {string} - */ - flagHash() { - return [ - this.deleted(), - this.isDeleted(), - this.isUnseen(), - this.isFlagged(), - this.isAnswered(), - this.isForwarded(), - this.isReadReceipt() - ].join(','); - } - } diff --git a/dev/Stores/User/Messagelist.js b/dev/Stores/User/Messagelist.js index d24848ade..1b3750664 100644 --- a/dev/Stores/User/Messagelist.js +++ b/dev/Stores/User/Messagelist.js @@ -332,11 +332,7 @@ MessagelistUserStore.removeMessagesFromList = ( ? messageList.filter(item => item && uidForRemove.includes(pInt(item.uid))) : []; - messages.forEach(item => { - if (item && item.isUnseen()) { - ++unseenCount; - } - }); + messages.forEach(item => item && item.isUnseen() && ++unseenCount); if (fromFolder && !copy) { fromFolder.messageCountAll( diff --git a/dev/Styles/User/Attachments.less b/dev/Styles/User/Attachments.less index 4ef482afd..93f73c80d 100644 --- a/dev/Styles/User/Attachments.less +++ b/dev/Styles/User/Attachments.less @@ -19,10 +19,6 @@ opacity: 0.6; } - &.checked { - box-shadow: 0 0 0 1px rgba(0, 0, 255, 0.1), 0 1px 5px rgba(0, 0, 255, 0.2); - } - .checkboxAttachment { bottom: 6px; cursor: pointer; diff --git a/dev/Styles/User/MessageList.less b/dev/Styles/User/MessageList.less index 81b4414e7..791f1904d 100644 --- a/dev/Styles/User/MessageList.less +++ b/dev/Styles/User/MessageList.less @@ -216,7 +216,7 @@ html:not(rl-mobile) { margin-right:5px } - &.deleted-mark { + &.flag-\\deleted { opacity: .7; .subjectParent { text-decoration: line-through; @@ -228,7 +228,7 @@ html:not(rl-mobile) { } .checkboxMessage { - padding: 0 6px; + margin: 0 6px; font-size: 16px; } @@ -242,8 +242,6 @@ html:not(rl-mobile) { .attachmentParent { position: relative; margin: 2px 10px 0 5px; - color: #666; - text-shadow: 0 1px 0 #eee; } .senderParent, .subjectParent { @@ -340,15 +338,15 @@ html:not(rl-mobile) { .flagParent { padding: 0 10px 0 5px; } - &.flagged .flagParent::after, + &.flag-\\flagged .flagParent::after, &.hasFlaggedSubMessage .flagParent::after { color: orange; content: '★'; /*⚑*/ } - &:not(.flagged):not(.hasFlaggedSubMessage) .flagParent::after { + &:not(.flag-\\flagged):not(.hasFlaggedSubMessage) .flagParent::after { content: '☆'; /*⚐*/ } - &:not(.flagged):not(.hasFlaggedSubMessage) .flagParent:not(:hover) { + &:not(.flag-\\flagged):not(.hasFlaggedSubMessage) .flagParent:not(:hover) { opacity: 0.5; } } @@ -392,7 +390,7 @@ html.rl-ctrl-key-pressed .messageListItem { .checkboxMessage { line-height: 12px; - margin: 10px 0 -5px; + margin: 10px 6px -5px; } .subjectParent { @@ -444,3 +442,35 @@ html:not(.rl-mobile):not(.rl-side-preview-pane) { } } } + +.senderParent::before { + font-family: snappymail; +} +.flag-\\answered .senderParent::before { + content: '← '; +} +.flag-\$forwarded .senderParent::before { + content: '→ '; +} +.flag-\\answered.flag-\$forwarded .senderParent::before { + content: '←→ '; +} + +/* Thunderbird labels */ +/* +.flag-\$label5 .checkboxMessage { background-color: #808; } +.flag-\$label4 .checkboxMessage { background-color: #00F; } +.flag-\$label3 .checkboxMessage { background-color: #080; } +.flag-\$label2 .checkboxMessage { background-color: #FA0; } +.flag-\$label1 .checkboxMessage { background-color: #F00; } +*/ +.messageListItem.flag-\$label5.focused { background-color: rgba(255, 0, 255, 0.30); } +.messageListItem.flag-\$label4.focused { background-color: rgba( 64, 64, 255, 0.30); } +.messageListItem.flag-\$label3.focused { background-color: rgba( 0, 255, 0, 0.30); } +.messageListItem.flag-\$label2.focused { background-color: rgba(255, 170, 0, 0.30); } +.messageListItem.flag-\$label1.focused { background-color: rgba(255, 0, 0, 0.30); } +.messageListItem.flag-\$label5:not(.focused) { color: #939; } +.messageListItem.flag-\$label4:not(.focused) { color: #33F; } +.messageListItem.flag-\$label3:not(.focused) { color: #090; } +.messageListItem.flag-\$label2:not(.focused) { color: #F90; } +.messageListItem.flag-\$label1:not(.focused) { color: #F00; } diff --git a/dev/Styles/User/MessageView.less b/dev/Styles/User/MessageView.less index c23deddeb..b39b406ff 100644 --- a/dev/Styles/User/MessageView.less +++ b/dev/Styles/User/MessageView.less @@ -42,13 +42,10 @@ html.rl-no-preview-pane { line-height: 70px; padding-top: 100px; color: #999; - - .icon-mail { - font-size: 100px; - font-size: 50px; - line-height: 90px; - padding-left: 10px; - } + } + .b-message-view-checked-helper::after { + content: ' ✉'; + font-family: snappymail; } .b-message-view-desc { @@ -303,14 +300,6 @@ html.rl-no-preview-pane { } } - &.unselectedAttachmentsError { - .attachmentItem { - box-shadow: 0 1px 4px red; - box-shadow: 0 1px 5px rgba(255, 0, 0, 0.4); - box-shadow: 0 0 0 1px rgba(255, 0, 0, 0.2), 0 1px 5px rgba(255, 0, 0, 0.3); - } - } - .controls-handle { position: absolute; bottom: 5px; diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index 8d67433e0..cc366ab4d 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -20,7 +20,7 @@ import { messagesDeleteHelper } from 'Common/Folders'; import { serverRequest } from 'Common/Links'; import { i18n, getNotification, getUploadErrorDescByCode, timestampToString } from 'Common/Translator'; import { MessageFlagsCache, setFolderHash } from 'Common/Cache'; -import { Settings, SettingsGet, elementById, addShortcut } from 'Common/Globals'; +import { Settings, SettingsCapa, SettingsGet, elementById, addShortcut } from 'Common/Globals'; //import { exitFullscreen, isFullscreen, toggleFullscreen } from 'Common/Fullscreen'; import { AppUserStore } from 'Stores/User/App'; @@ -236,6 +236,7 @@ export class ComposePopupView extends AbstractViewPopup { this.sLastFocusedField = 'to'; this.allowContacts = AppUserStore.allowContacts(); + this.allowIdentities = SettingsCapa('Identities'); this.bSkipNextHide = false; diff --git a/dev/View/User/MailBox/MessageList.js b/dev/View/User/MailBox/MessageList.js index 4b5f6595a..b49fd1dc9 100644 --- a/dev/View/User/MailBox/MessageList.js +++ b/dev/View/User/MailBox/MessageList.js @@ -571,21 +571,12 @@ export class MailMessageList extends AbstractViewRight { seenMessagesFast(seen) { const checked = MessagelistUserStore.listCheckedOrSelected(); - if (checked.length) { - if (undefined === seen) { - const unseen = checked.filter(message => message.isUnseen()); - listAction( - checked[0].folder, - unseen.length ? MessageSetAction.SetSeen : MessageSetAction.UnsetSeen, - checked - ); - } else { - listAction( - checked[0].folder, - seen ? MessageSetAction.SetSeen : MessageSetAction.UnsetSeen, - checked - ); - } + if (checked.length && null != seen) { + listAction( + checked[0].folder, + seen ? MessageSetAction.SetSeen : MessageSetAction.UnsetSeen, + checked + ); } } diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js index d66b06254..75768be08 100644 --- a/dev/View/User/MailBox/MessageView.js +++ b/dev/View/User/MailBox/MessageView.js @@ -123,9 +123,6 @@ export class MailMessageView extends AbstractViewRight { this.fullScreenMode = isFullscreen; this.toggleFullScreen = toggleFullscreen; - this.messageListOfThreadsLoading = ko.observable(false).extend({ rateLimit: 1 }); - this.highlightUnselectedAttachments = ko.observable(false).extend({ falseTimeout: 2000 }); - this.downloadAsZipError = ko.observable(false).extend({ falseTimeout: 7000 }); this.messageDomFocused = ko.observable(false).extend({ rateLimit: 0 }); @@ -136,7 +133,10 @@ export class MailMessageView extends AbstractViewRight { this.addComputables({ allowAttachmentControls: () => arrayLength(attachmentsActions) && SettingsCapa('AttachmentsActions'), - downloadAsZipAllowed: () => this.attachmentsActions.includes('zip') && this.allowAttachmentControls(), + downloadAsZipAllowed: () => this.attachmentsActions.includes('zip') + && (currentMessage() ? currentMessage().attachments : []) + .filter(item => item && !item.isLinked() && item.checked() && item.download) + .length, lastReplyAction: { read: this.lastReplyAction_, @@ -538,8 +538,6 @@ export class MailMessageView extends AbstractViewRight { } }) .catch(() => this.downloadAsZipError(true)); - } else { - this.highlightUnselectedAttachments(true); } } @@ -566,20 +564,20 @@ export class MailMessageView extends AbstractViewRight { readReceipt() { let oMessage = currentMessage() if (oMessage.readReceipt()) { - Remote.request('SendReadReceiptMessage', null, { + Remote.request('SendReadReceiptMessage', iError => { + if (!iError) { + oMessage.flags.push('$mdnsent'); +// oMessage.flags.valueHasMutated(); + MessageFlagsCache.store(oMessage); + MessagelistUserStore.reloadFlagsAndCachedMessage(); + } + }, { MessageFolder: oMessage.folder, MessageUid: oMessage.uid, ReadReceipt: oMessage.readReceipt(), Subject: i18n('READ_RECEIPT/SUBJECT', { SUBJECT: oMessage.subject() }), Text: i18n('READ_RECEIPT/BODY', { 'READ-RECEIPT': AccountUserStore.email() }) }); - - oMessage.flags.push('$mdnsent'); -// oMessage.flags.valueHasMutated(); - - MessageFlagsCache.store(oMessage); - - MessagelistUserStore.reloadFlagsAndCachedMessage(); } } diff --git a/integrations/cloudron/DESCRIPTION.md b/integrations/cloudron/DESCRIPTION.md index f496250f0..f59fbbfb9 100644 --- a/integrations/cloudron/DESCRIPTION.md +++ b/integrations/cloudron/DESCRIPTION.md @@ -1,4 +1,4 @@ -This app packages SnappyMail 2.16.1. +This app packages SnappyMail 2.16.3. SnappyMail is a simple, modern, lightweight & fast web-based email client. diff --git a/integrations/cloudron/Dockerfile b/integrations/cloudron/Dockerfile index 93b3dacde..0e65debbf 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.16.1 +VERSION=2.16.3 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/VERSION b/integrations/nextcloud/snappymail/VERSION index 15c5c9d5b..38275d2df 100755 --- a/integrations/nextcloud/snappymail/VERSION +++ b/integrations/nextcloud/snappymail/VERSION @@ -1 +1 @@ -2.16.1 \ No newline at end of file +2.16.3 \ No newline at end of file diff --git a/integrations/nextcloud/snappymail/appinfo/info.xml b/integrations/nextcloud/snappymail/appinfo/info.xml index a781d80fd..b6369e00a 100644 --- a/integrations/nextcloud/snappymail/appinfo/info.xml +++ b/integrations/nextcloud/snappymail/appinfo/info.xml @@ -4,7 +4,7 @@ SnappyMail SnappyMail Webmail Simple, modern and fast web-based email client. After enabling in Nextcloud, go to Nextcloud admin panel, "Additionnal settings" and you will see a "SnappyMail webmail" section. There, click on the link to go to the SnappyMail admin panel. The default user/password is admin/12345. This version is based on SnappyMail 2.6.0 (2021-07). - 2.16.1 + 2.16.3 agpl SnappyMail Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli SnappyMail diff --git a/integrations/virtualmin/snappymail.pl b/integrations/virtualmin/snappymail.pl index 3987b5f50..be2cf24bd 100644 --- a/integrations/virtualmin/snappymail.pl +++ b/integrations/virtualmin/snappymail.pl @@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a # script_snappymail_versions() sub script_snappymail_versions { -return ( "2.16.1" ); +return ( "2.16.3" ); } sub script_snappymail_version_desc diff --git a/package.json b/package.json index 4bc01b4b0..2ea4e0581 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "title": "SnappyMail", "description": "Simple, modern & fast web-based email client", "private": true, - "version": "2.16.1", + "version": "2.16.3", "homepage": "https://snappymail.eu", "author": { "name": "DJ Maze", diff --git a/plugins/change-password-froxlor/driver.php b/plugins/change-password-froxlor/driver.php new file mode 100644 index 000000000..0e26d32c3 --- /dev/null +++ b/plugins/change-password-froxlor/driver.php @@ -0,0 +1,107 @@ +oConfig = $oConfig; + $this->oLogger = $oLogger; + } + + public static function isSupported() : bool + { + return \class_exists('PDO', false) + // The PHP extension PDO (mysql) must be installed to use this plugin + && \in_array('mysql', \PDO::getAvailableDrivers()); + } + + public static function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('froxlor_dsn')->SetLabel('Froxlor PDO dsn') + ->SetDefaultValue('mysql:host=localhost;dbname=froxlor;charset=utf8'), + \RainLoop\Plugins\Property::NewInstance('froxlor_user')->SetLabel('User'), + \RainLoop\Plugins\Property::NewInstance('froxlor_password')->SetLabel('Password') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD), + \RainLoop\Plugins\Property::NewInstance('froxlor_allowed_emails')->SetLabel('Allowed emails') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@domain1.net user2@domain1.net *@domain2.net') + ->SetDefaultValue('*') + ); + } + + public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool + { + if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'froxlor_allowed_emails', ''))) { + return false; + } + + try + { + if ($this->oLogger) { + $this->oLogger->Write('froxlor: Try to change password for '.$oAccount->Email()); + } + + $oPdo = new \PDO( + $this->oConfig->Get('plugin', 'froxlor_dsn', ''), + $this->oConfig->Get('plugin', 'froxlor_user', ''), + $this->oConfig->Get('plugin', 'froxlor_password', ''), + array( + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION + ) + ); + + $oStmt = $oPdo->prepare('SELECT password_enc, id FROM mail_users WHERE username = ? LIMIT 1'); + + if ($oStmt->execute(array($oAccount->IncLogin()))) { + $aFetchResult = $oStmt->fetch(\PDO::FETCH_ASSOC); + if (!empty($aFetchResult['id'])) { + $sDbPassword = $aFetchResult['password_enc']; + $sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$')); + if (\crypt($sPrevPassword, $sDbSalt) === $sDbPassword) { + + $oStmt = $oPdo->prepare('UPDATE mail_users SET password_enc = ? WHERE id = ?'); + + return !!$oStmt->execute(array( + $this->cryptPassword($sNewPassword), + $aFetchResult['id'] + )); + } + } + } + } + catch (\Exception $oException) + { + if ($this->oLogger) { + $this->oLogger->WriteException($oException); + } + } + return false; + } + + private function cryptPassword(string $sPassword) : string + { + if (\defined('CRYPT_SHA512') && CRYPT_SHA512) { + $sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$'; + } elseif (\defined('CRYPT_SHA256') && CRYPT_SHA256) { + $sSalt = '$5$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$'; + } else { + $sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$'; + } + return \crypt($sPassword, $sSalt); + } +} diff --git a/plugins/change-password-froxlor/index.php b/plugins/change-password-froxlor/index.php new file mode 100644 index 000000000..9e0bbd160 --- /dev/null +++ b/plugins/change-password-froxlor/index.php @@ -0,0 +1,20 @@ +addHook('smtp.credentials', 'FilterSmtpCredentials'); + $this->addHook('smtp.before-connect', 'FilterSmtpCredentials'); } /** * @param \RainLoop\Model\Account $oAccount * @param array $aSmtpCredentials */ - public function FilterSmtpCredentials($oAccount, &$aSmtpCredentials) + public function FilterSmtpCredentials(\RainLoop\Model\Account $oAccount, + \MailSo\Smtp\SmtpClient $oSmtpClient, + array &$aSmtpCredentials) { - if ($oAccount instanceof \RainLoop\Model\Account && \is_array($aSmtpCredentials)) - { - // Default: - // $aSmtpCredentials['Ehlo'] = \MailSo\Smtp\SmtpClient::EhloHelper(); - // - // or write your custom php - $aSmtpCredentials['Ehlo'] = 'localhost'; - } + // Default: + // $aSmtpCredentials['Ehlo'] = \MailSo\Smtp\SmtpClient::EhloHelper(); + // + // or write your custom php + $aSmtpCredentials['Ehlo'] = 'localhost'; } } diff --git a/plugins/contacts-suggestions-example/ContactsExampleSuggestions.php b/plugins/contacts-suggestions-example/ContactsExampleSuggestions.php deleted file mode 100644 index c87ed2f61..000000000 --- a/plugins/contacts-suggestions-example/ContactsExampleSuggestions.php +++ /dev/null @@ -1,21 +0,0 @@ -Email(), ''), - array('email@domain.com', 'name') - ); - - return $aResult; - } -} diff --git a/plugins/contacts-suggestions-example/LICENSE b/plugins/contacts-suggestions-example/LICENSE deleted file mode 100644 index 4aed64b3a..000000000 --- a/plugins/contacts-suggestions-example/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 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/contacts-suggestions-example/index.php b/plugins/contacts-suggestions-example/index.php deleted file mode 100644 index 3e505c455..000000000 --- a/plugins/contacts-suggestions-example/index.php +++ /dev/null @@ -1,35 +0,0 @@ -addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @param string $sName - * @param mixed $mResult - */ - public function MainFabrica($sName, &$mResult) - { - switch ($sName) - { - case 'suggestions': - - if (!\is_array($mResult)) - { - $mResult = array(); - } - - include_once __DIR__.'/ContactsExampleSuggestions.php'; - $mResult[] = new ContactsExampleSuggestions(); - break; - } - } -} diff --git a/plugins/custom-admin-settings-tab/index.php b/plugins/custom-admin-settings-tab/index.php deleted file mode 100644 index 306db50fa..000000000 --- a/plugins/custom-admin-settings-tab/index.php +++ /dev/null @@ -1,34 +0,0 @@ -UseLangs(true); // start use langs folder - - $this->addJs('js/CustomAdminSettings.js', true); // add js file - - $this->addJsonHook('JsonAdminGetData', 'JsonAdminGetData'); - - $this->addTemplate('templates/PluginCustomAdminSettingsTab.html', true); - } - - /** - * @return array - */ - public function JsonAdminGetData() - { - return $this->jsonResponse(__FUNCTION__, array( - 'PHP' => phpversion() - )); - } -} - diff --git a/plugins/custom-admin-settings-tab/js/CustomAdminSettings.js b/plugins/custom-admin-settings-tab/js/CustomAdminSettings.js deleted file mode 100644 index 20bd0d6f5..000000000 --- a/plugins/custom-admin-settings-tab/js/CustomAdminSettings.js +++ /dev/null @@ -1,44 +0,0 @@ - -(function () { - - if (!window.rl) - { - return; - } - - /** - * @constructor - */ - function CustomAdminSettings() - { - this.php = ko.observable(''); - - this.loading = ko.observable(false); - } - - CustomAdminSettings.prototype.onBuild = function () // special function - { - var self = this; - - this.loading(true); - - rl.pluginRemoteRequest((iError, oData) => { - - self.loading(false); - - if (!iError) { - self.php(oData.Result.PHP || ''); - } - - if (rl.Enums.StorageResultType.Abort === iError) { - // show abort - } - - }, 'JsonAdminGetData'); - - }; - - rl.addSettingsViewModelForAdmin(CustomAdminSettings, 'PluginCustomAdminSettingsTab', - 'SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN/TAB_NAME', 'custom'); - -}()); diff --git a/plugins/custom-admin-settings-tab/langs/en.ini b/plugins/custom-admin-settings-tab/langs/en.ini deleted file mode 100644 index 3d81ab706..000000000 --- a/plugins/custom-admin-settings-tab/langs/en.ini +++ /dev/null @@ -1,4 +0,0 @@ -[SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN] -TAB_NAME = "Custom Extension Tab" -LEGEND_CUSTOM = "Custom Extension Tab (Example)" -LABEL_PHP_VERSION = "PHP version" diff --git a/plugins/custom-admin-settings-tab/langs/ru.ini b/plugins/custom-admin-settings-tab/langs/ru.ini deleted file mode 100644 index 71fcee232..000000000 --- a/plugins/custom-admin-settings-tab/langs/ru.ini +++ /dev/null @@ -1,4 +0,0 @@ -[SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN] -TAB_NAME = "Плагин" -LEGEND_CUSTOM = "Плагин (Пример)" -LABEL_PHP_VERSION = "PHP версия" \ No newline at end of file diff --git a/plugins/custom-auth-example/LICENSE b/plugins/custom-auth-example/LICENSE deleted file mode 100644 index 271342337..000000000 --- a/plugins/custom-auth-example/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 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/custom-auth-example/index.php b/plugins/custom-auth-example/index.php deleted file mode 100644 index 242443500..000000000 --- a/plugins/custom-auth-example/index.php +++ /dev/null @@ -1,44 +0,0 @@ -addHook('login.credentials', 'FilterLoginCredentials'); - } - - /** - * @param string $sEmail - * @param string $sLogin - * @param string $sPassword - * - * @throws \RainLoop\Exceptions\ClientException - */ - public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword) - { - // Your custom php logic - // You may change login credentials - if ('demo@snappymail.eu' === $sEmail) - { - $sEmail = 'user@snappymail.eu'; - $sLogin = 'user@snappymail.eu'; - $sPassword = 'super-puper-password'; - } - else - { - // or throw auth exeption - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AuthError); - // or - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountNotAllowed); - // or - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DomainNotAllowed); - } - } -} diff --git a/plugins/custom-settings-tab/LICENSE b/plugins/custom-settings-tab/LICENSE deleted file mode 100644 index 4aed64b3a..000000000 --- a/plugins/custom-settings-tab/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 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/custom-settings-tab/index.php b/plugins/custom-settings-tab/index.php deleted file mode 100644 index cee8ecf12..000000000 --- a/plugins/custom-settings-tab/index.php +++ /dev/null @@ -1,62 +0,0 @@ -UseLangs(true); // start use langs folder - - $this->addJs('js/CustomUserSettings.js'); // add js file - - $this->addJsonHook('JsonGetCustomUserData', 'JsonGetCustomUserData'); - $this->addJsonHook('JsonSaveCustomUserData', 'JsonSaveCustomUserData'); - - $this->addTemplate('templates/PluginCustomSettingsTab.html'); - } - - /** - * @return array - */ - public function JsonGetCustomUserData() - { - $aSettings = $this->getUserSettings(); - - $sUserFacebook = isset($aSettings['UserFacebook']) ? $aSettings['UserFacebook'] : ''; - $sUserSkype = isset($aSettings['UserSkype']) ? $aSettings['UserSkype'] : ''; - - // or get user's data from your custom storage ( DB / LDAP / ... ). - - \sleep(1); - return $this->jsonResponse(__FUNCTION__, array( - 'UserFacebook' => $sUserFacebook, - 'UserSkype' => $sUserSkype - )); - } - - /** - * @return array - */ - public function JsonSaveCustomUserData() - { - $sUserFacebook = $this->jsonParam('UserFacebook'); - $sUserSkype = $this->jsonParam('UserSkype'); - - // or put user's data to your custom storage ( DB / LDAP / ... ). - - \sleep(1); - return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings(array( - 'UserFacebook' => $sUserFacebook, - 'UserSkype' => $sUserSkype - ))); - } - -} - diff --git a/plugins/custom-settings-tab/js/CustomUserSettings.js b/plugins/custom-settings-tab/js/CustomUserSettings.js deleted file mode 100644 index 85b7f5241..000000000 --- a/plugins/custom-settings-tab/js/CustomUserSettings.js +++ /dev/null @@ -1,81 +0,0 @@ - -(function () { - - if (!window.rl) - { - return; - } - - /** - * @constructor - */ - function CustomUserSettings() - { - this.userSkype = ko.observable(''); - this.userFacebook = ko.observable(''); - - this.loading = ko.observable(false); - this.saving = ko.observable(false); - - this.savingOrLoading = ko.computed(function () { - return this.loading() || this.saving(); - }, this); - } - - CustomUserSettings.prototype.customJsonSaveData = function () - { - var self = this; - - if (this.saving()) - { - return false; - } - - this.saving(true); - - rl.pluginRemoteRequest((iError, oData) => { - - self.saving(false); - - if (!iError) - { - // true - } - else if (rl.Enums.StorageResultType.Abort === iError) { - // show abort - } - else - { - // false - } - - }, 'JsonSaveCustomUserData', { - 'UserSkype': this.userSkype(), - 'UserFacebook': this.userFacebook() - }); - }; - - CustomUserSettings.prototype.onBuild = function () // special function - { - var self = this; - - this.loading(true); - - rl.pluginRemoteRequest((iError, oData) => { - - self.loading(false); - - if (!iError) - { - self.userSkype(oData.Result.UserSkype || ''); - self.userFacebook(oData.Result.UserFacebook || ''); - } - - }, 'JsonGetCustomUserData'); - - }; - - rl.addSettingsViewModel(CustomUserSettings, 'PluginCustomSettingsTab', - 'SETTINGS_CUSTOM_PLUGIN/TAB_NAME', 'custom'); - -}()); diff --git a/plugins/custom-settings-tab/langs/en.ini b/plugins/custom-settings-tab/langs/en.ini deleted file mode 100644 index cb517b757..000000000 --- a/plugins/custom-settings-tab/langs/en.ini +++ /dev/null @@ -1,6 +0,0 @@ -[SETTINGS_CUSTOM_PLUGIN] -TAB_NAME = "Custom Extension" -LEGEND_CUSTOM = "Custom Extension (Example)" -LABEL_SKYPE = "Skype" -LABEL_FACEBOOK = "Facebook" -BUTTON_SAVE = "Save" diff --git a/plugins/custom-settings-tab/langs/ru.ini b/plugins/custom-settings-tab/langs/ru.ini deleted file mode 100644 index 6b275600c..000000000 --- a/plugins/custom-settings-tab/langs/ru.ini +++ /dev/null @@ -1,6 +0,0 @@ -[SETTINGS_CUSTOM_PLUGIN] -TAB_NAME = "Плагин" -LEGEND_CUSTOM = "Плагин (Пример)" -LABEL_SKYPE = "Skype" -LABEL_FACEBOOK = "Facebook" -BUTTON_SAVE = "Сохранить" \ No newline at end of file diff --git a/plugins/custom-admin-settings-tab/LICENSE b/plugins/example/LICENSE similarity index 100% rename from plugins/custom-admin-settings-tab/LICENSE rename to plugins/example/LICENSE diff --git a/plugins/example/index.php b/plugins/example/index.php index 40741b9cc..7bb6a2b68 100644 --- a/plugins/example/index.php +++ b/plugins/example/index.php @@ -31,6 +31,22 @@ class ExamplePlugin extends \RainLoop\Plugins\AbstractPlugin $this->addTemplate(string $sFile, bool $bAdminScope = false); $this->addTemplateHook(string $sName, string $sPlace, string $sLocalTemplateName, bool $bPrepend = false); */ + +// $this->addHook('login.credentials', 'FilterLoginCredentials'); + + + $this->UseLangs(true); // start use langs folder + + // User Settings tab + $this->addJs('js/ExampleUserSettings.js'); // add js file + $this->addJsonHook('JsonGetExampleUserData', 'JsonGetExampleUserData'); + $this->addJsonHook('JsonSaveExampleUserData', 'JsonSaveExampleUserData'); + $this->addTemplate('templates/ExampleUserSettingsTab.html'); + + // Admin Settings tab + $this->addJs('js/ExampleAdminSettings.js', true); // add js file + $this->addJsonHook('JsonAdminGetData', 'JsonAdminGetData'); + $this->addTemplate('templates/ExampleAdminSettingsTab.html', true); } /** @@ -61,6 +77,77 @@ class ExamplePlugin extends \RainLoop\Plugins\AbstractPlugin } } + public function JsonAdminGetData() + { + return $this->jsonResponse(__FUNCTION__, array( + 'PHP' => \phpversion() + )); + } + + /** + * @param string $sEmail + * @param string $sLogin + * @param string $sPassword + * + * @throws \RainLoop\Exceptions\ClientException + */ + public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword) + { + // Your custom php logic + // You may change login credentials + if ('demo@snappymail.eu' === $sEmail) + { + $sEmail = 'user@snappymail.eu'; + $sLogin = 'user@snappymail.eu'; + $sPassword = 'super-duper-password'; + } + else + { + // or throw auth exeption + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AuthError); + // or + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountNotAllowed); + // or + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DomainNotAllowed); + } + } + + /** + * @return array + */ + public function JsonGetExampleUserData() + { + $aSettings = $this->getUserSettings(); + + $sUserFacebook = isset($aSettings['UserFacebook']) ? $aSettings['UserFacebook'] : ''; + $sUserSkype = isset($aSettings['UserSkype']) ? $aSettings['UserSkype'] : ''; + + // or get user's data from your custom storage ( DB / LDAP / ... ). + + \sleep(1); + return $this->jsonResponse(__FUNCTION__, array( + 'UserFacebook' => $sUserFacebook, + 'UserSkype' => $sUserSkype + )); + } + + /** + * @return array + */ + public function JsonSaveExampleUserData() + { + $sUserFacebook = $this->jsonParam('UserFacebook'); + $sUserSkype = $this->jsonParam('UserSkype'); + + // or put user's data to your custom storage ( DB / LDAP / ... ). + + \sleep(1); + return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings(array( + 'UserFacebook' => $sUserFacebook, + 'UserSkype' => $sUserSkype + ))); + } + /* public function Config() : \RainLoop\Config\Plugin public function Description() : string diff --git a/plugins/example/js/ExampleAdminSettings.js b/plugins/example/js/ExampleAdminSettings.js new file mode 100644 index 000000000..5a52ecfda --- /dev/null +++ b/plugins/example/js/ExampleAdminSettings.js @@ -0,0 +1,37 @@ + +(rl => { if (rl) { + + class ExampleAdminSettings + { + constructor() + { + this.php = ko.observable(''); + this.loading = ko.observable(false); + } + + onBuild() + { + this.loading(true); + + rl.pluginRemoteRequest((iError, oData) => { + + this.loading(false); + + if (iError) { + console.error({ + iError: iError, + oData: oData + }); + } else { + this.php(oData.Result.PHP || ''); + } + + }, 'JsonAdminGetData'); + + } + } + + rl.addSettingsViewModelForAdmin(ExampleAdminSettings, 'ExampleAdminSettingsTab', + 'SETTINGS_EXAMPLE_ADMIN_EXAMPLE_TAB_PLUGIN/TAB_NAME', 'Example'); + +}})(window.rl); diff --git a/plugins/example/js/ExampleUserSettings.js b/plugins/example/js/ExampleUserSettings.js new file mode 100644 index 000000000..839e9e84a --- /dev/null +++ b/plugins/example/js/ExampleUserSettings.js @@ -0,0 +1,68 @@ + +(rl => { if (rl) { + + class ExampleUserSettings + { + constructor() + { + this.userSkype = ko.observable(''); + this.userFacebook = ko.observable(''); + + this.loading = ko.observable(false); + this.saving = ko.observable(false); + + this.savingOrLoading = ko.computed(() => { + return this.loading() || this.saving(); + }); + } + + exampleJsonSaveData() + { + if (!this.saving()) { + this.saving(true); + + rl.pluginRemoteRequest((iError, oData) => { + + this.saving(false); + + if (iError) { + console.error({ + iError: iError, + oData: oData + }); + } else { + console.dir({ + iError: iError, + oData: oData + }); + } + + }, 'JsonSaveExampleUserData', { + 'UserSkype': this.userSkype(), + 'UserFacebook': this.userFacebook() + }); + } + } + + onBuild() + { + this.loading(true); + + rl.pluginRemoteRequest((iError, oData) => { + + this.loading(false); + + if (!iError) { + self.userSkype(oData.Result.UserSkype || ''); + self.userFacebook(oData.Result.UserFacebook || ''); + } + + }, 'JsonGetExampleUserData'); + + } + } + + rl.addSettingsViewModel(ExampleUserSettings, 'ExampleUserSettingsTab', + 'SETTINGS_EXAMPLE_PLUGIN/TAB_NAME', 'Example'); + +}})(window.rl); diff --git a/plugins/example/langs/en.json b/plugins/example/langs/en.json new file mode 100644 index 000000000..9c5f0c84c --- /dev/null +++ b/plugins/example/langs/en.json @@ -0,0 +1,15 @@ +{ + "SETTINGS_EXAMPLE_ADMIN_EXAMPLE_TAB_PLUGIN": { + "TAB_NAME": "Example Extension Tab", + "LEGEND_EXAMPLE": "Example Extension Tab (Example)", + "LABEL_PHP_VERSION": "PHP version" + }, + "SETTINGS_EXAMPLE_PLUGIN": { + "TAB_NAME": "Example Extension", + "LEGEND_EXAMPLE": "Example Extension (Example)", + "LABEL_PHP_VERSION": "PHP version", + "LABEL_SKYPE": "Skype", + "LABEL_FACEBOOK": "Facebook", + "BUTTON_SAVE": "Save" + } +} diff --git a/plugins/custom-admin-settings-tab/templates/PluginCustomAdminSettingsTab.html b/plugins/example/templates/ExampleAdminSettingsTab.html similarity index 64% rename from plugins/custom-admin-settings-tab/templates/PluginCustomAdminSettingsTab.html rename to plugins/example/templates/ExampleAdminSettingsTab.html index cab9229a7..8fc98c5fc 100644 --- a/plugins/custom-admin-settings-tab/templates/PluginCustomAdminSettingsTab.html +++ b/plugins/example/templates/ExampleAdminSettingsTab.html @@ -1,17 +1,17 @@
- +    
-
\ No newline at end of file + diff --git a/plugins/custom-settings-tab/templates/PluginCustomSettingsTab.html b/plugins/example/templates/ExampleUserSettingsTab.html similarity index 66% rename from plugins/custom-settings-tab/templates/PluginCustomSettingsTab.html rename to plugins/example/templates/ExampleUserSettingsTab.html index 1b688168b..3bbfad069 100644 --- a/plugins/custom-settings-tab/templates/PluginCustomSettingsTab.html +++ b/plugins/example/templates/ExampleUserSettingsTab.html @@ -1,13 +1,13 @@
- +    
@@ -15,7 +15,7 @@
@@ -23,10 +23,10 @@
-
diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Messages.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Messages.php index 77bb81db9..78109fe8d 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Messages.php @@ -402,12 +402,12 @@ trait Messages * @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception */ - public function MessageSimpleThread(string $sSearchCriterias = 'ALL', bool $bReturnUid = true) : array + public function MessageSimpleThread(string $sSearchCriterias = 'ALL', bool $bReturnUid = true) : iterable { $oThread = new \MailSo\Imap\Requests\THREAD($this); $oThread->sCriterias = $sSearchCriterias; $oThread->bUid = $bReturnUid; - return $oThread->SendRequestGetResponse(); + yield from $oThread->SendRequestIterateResponse(); } private function getSimpleESearchOrESortResult(bool $bReturnUid) : array diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Quota.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Quota.php index eb8fa4223..4d91d6dfd 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Quota.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Quota.php @@ -28,9 +28,7 @@ trait Quota */ public function Quota(string $sRootName = '') : ?array { - return $this->IsSupported('QUOTA') - ? $this->getQuotaResult($this->SendRequestGetResponse("GETQUOTA {$this->EscapeFolderName($sRootName)}")) - : null; + return $this->getQuota(false, $sRootName); } /** @@ -41,18 +39,19 @@ trait Quota */ public function QuotaRoot(string $sFolderName = 'INBOX') : ?array { - return $this->IsSupported('QUOTA') - ? $this->getQuotaResult($this->SendRequestGetResponse("GETQUOTAROOT {$this->EscapeFolderName($sFolderName)}")) - : null; + return $this->getQuota(true, $sFolderName); } // * QUOTA "User quota" (STORAGE 1284645 2097152)\r\n - private function getQuotaResult(\MailSo\Imap\ResponseCollection $oResponseCollection) : array + private function getQuota(bool $root, string $sFolderName) : ?array { + if (!$this->IsSupported('QUOTA')) { + return null; + } + $oResponseCollection = $this->SendRequestGetResponse(($root?'GETQUOTAROOT':'GETQUOTA') . " {$this->EscapeFolderName($sFolderName)}"); $aReturn = array(0, 0); - foreach ($oResponseCollection as $oResponse) { - if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType - && 'QUOTA' === $oResponse->StatusOrIndex + foreach ($this->yieldUntaggedResponses() as $oResponse) { + if ('QUOTA' === $oResponse->StatusOrIndex && isset($oResponse->ResponseList[3]) && \is_array($oResponse->ResponseList[3]) && 2 < \count($oResponse->ResponseList[3]) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/MessageFlag.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/MessageFlag.php index c2e2c2b69..c42865ed8 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/MessageFlag.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/MessageFlag.php @@ -7,6 +7,8 @@ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. + * + * https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml */ namespace MailSo\Imap\Enumerations; @@ -18,10 +20,17 @@ namespace MailSo\Imap\Enumerations; */ abstract class MessageFlag { -// const RECENT = '\\Recent'; // IMAP4rev2 deprecated - const SEEN = '\\Seen'; - const DELETED = '\\Deleted'; - const FLAGGED = '\\Flagged'; - const ANSWERED = '\\Answered'; - const DRAFT = '\\Draft'; + const +// RECENT = '\\Recent', // IMAP4rev2 deprecated + SEEN = '\\Seen', + DELETED = '\\Deleted', + FLAGGED = '\\Flagged', + ANSWERED = '\\Answered', + DRAFT = '\\Draft', + // https://datatracker.ietf.org/doc/html/rfc9051#section-2.3.2 + FORWARDED = '$Forwarded', + MDNSENT = '$MDNSent', + JUNK = '$Junk', + NOTJUNK = '$NotJunk', + PHISHING = '$Phishing'; } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/StoreAction.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/StoreAction.php index 4c6d8b44e..172e0e89b 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/StoreAction.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/StoreAction.php @@ -18,8 +18,8 @@ namespace MailSo\Imap\Enumerations; */ abstract class StoreAction { - const SET_FLAGS = 'FLAGS'; - const SET_FLAGS_SILENT = 'FLAGS.SILENT'; +// const SET_FLAGS = 'FLAGS'; +// const SET_FLAGS_SILENT = 'FLAGS.SILENT'; const ADD_FLAGS = '+FLAGS'; const ADD_FLAGS_SILENT = '+FLAGS.SILENT'; const REMOVE_FLAGS = '-FLAGS'; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderInformation.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderInformation.php index dcedd1860..cee0a0c9d 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderInformation.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderInformation.php @@ -15,7 +15,7 @@ namespace MailSo\Imap; * @category MailSo * @package Imap */ -class FolderInformation +class FolderInformation implements \JsonSerializable { use Traits\Status; @@ -36,6 +36,7 @@ class FolderInformation /** * @var array + * NOTE: Empty when FolderExamine is used */ public $PermanentFlags = array(); @@ -57,4 +58,23 @@ class FolderInformation \in_array($sFlag, $this->PermanentFlags) || \in_array($sFlag, $this->Flags); } + + public function jsonSerialize() + { + return array( + 'Name' => $this->FolderName, + 'Flags' => $this->Flags, + 'PermanentFlags' => $this->PermanentFlags, +/* + 'Messages' => $this->MESSAGES, + 'Unseen' => $this->UNSEEN, + 'Recent' => $this->RECENT, + 'UidNext' => $this->UIDNEXT, + 'UidValidity' => $this->UIDVALIDITY, + 'Highestmodseq' => $this->HIGHESTMODSEQ, + 'Appendlimit' => $this->APPENDLIMIT, + 'Mailboxid' => $this->MAILBOXID, +*/ + ); + } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php index 017724e5e..818258c1c 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php @@ -41,7 +41,7 @@ class THREAD extends Request parent::__construct($oImapClient); } - public function SendRequestGetResponse() : array + public function SendRequestIterateResponse() : iterable { if (!$this->oImapClient->IsSupported(\strtoupper("THREAD={$this->sAlgorithm}"))) { $this->oImapClient->writeLogException( @@ -58,7 +58,6 @@ class THREAD extends Request ) ); - $aReturn = array(); foreach ($this->oImapClient->yieldUntaggedResponses() as $oResponse) { $iOffset = ($this->bUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && 'THREAD' === $oResponse->ResponseList[2]) ? 1 : 0; if (('THREAD' === $oResponse->StatusOrIndex || $iOffset) @@ -69,12 +68,11 @@ class THREAD extends Request for ($iIndex = 2 + $iOffset; $iIndex < $iLen; ++$iIndex) { $aNewValue = $this->validateThreadItem($oResponse->ResponseList[$iIndex]); if (\is_array($aNewValue)) { - $aReturn[] = $aNewValue; + yield $aNewValue; } } } } - return $aReturn; } /** diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Folder.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Folder.php index 1dbde14e8..14f777b39 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Folder.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Folder.php @@ -228,6 +228,7 @@ class Folder implements \JsonSerializable 'Selectable' => $this->IsSelectable(), 'Flags' => $this->FlagsLowerCase(), // 'Extended' => $aExtended, +// 'PermanentFlags' => $this->oImapFolder->PermanentFlags, 'Metadata' => $this->oImapFolder->Metadata() ); } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php index 3fbec55da..ea778475c 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php @@ -521,21 +521,19 @@ class MailClient */ public function FolderInformation(string $sFolderName, int $iPrevUidNext = 0, SequenceSet $oRange = null) : array { - $aFlags = array(); - list($iCount, $iUnseenCount, $iUidNext, $iHighestModSeq, $iAppendLimit, $sMailboxId) = $this->initFolderValues($sFolderName); - if ($oRange && \count($oRange)) - { - $this->oImapClient->FolderExamine($sFolderName); + $aFlags = array(); + if ($oRange && \count($oRange)) { + $oInfo = $this->oImapClient->FolderExamine($sFolderName); + // $oInfo->PermanentFlags $aFetchResponse = $this->oImapClient->Fetch(array( FetchType::UID, FetchType::FLAGS ), (string) $oRange, $oRange->UID); - foreach ($aFetchResponse as $oFetchResponse) - { + foreach ($aFetchResponse as $oFetchResponse) { $iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID); $aLowerFlags = \array_map('strtolower', $oFetchResponse->GetFetchValue(FetchType::FLAGS)); $aFlags[] = array( @@ -620,10 +618,15 @@ class MailClient $this->oImapClient->FolderExamine($sFolderName); - $aThreadUids = array(); + $aResult = array(); try { - $aThreadUids = $this->oImapClient->MessageSimpleThread($sSearchHash); + foreach ($this->oImapClient->MessageSimpleThread($sSearchHash) as $mItem) { + // Flatten to single level + $aMap = []; + \array_walk_recursive($mItem, function($a) use (&$aMap) { $aMap[] = $a; }); + $aResult[] = $aMap; + } } catch (\MailSo\Imap\Exceptions\RuntimeException $oException) { @@ -631,14 +634,6 @@ class MailClient unset($oException); } - // Flatten to single levels - $aResult = array(); - foreach ($aThreadUids as $mItem) { - $aMap = []; - \array_walk_recursive($mItem, function($a) use (&$aMap) { $aMap[] = $a; }); - $aResult[] = $aMap; - } - if ($oCacher && $oCacher->IsInited() && !empty($sSerializedHashKey)) { $oCacher->Set($sSerializedHashKey, \json_encode(array( @@ -830,15 +825,19 @@ class MailClient list($iMessageRealCount, $iMessageUnseenCount, $iUidNext, $iHighestModSeq) = $this->initFolderValues($oParams->sFolderName); - $this->oImapClient->FolderExamine($oParams->sFolderName); + // Don't use FolderExamine, else PERMANENTFLAGS is empty + $oInfo = $this->oImapClient->FolderSelect($oParams->sFolderName); $oMessageCollection = new MessageCollection; $oMessageCollection->FolderName = $oParams->sFolderName; + $oMessageCollection->FolderInfo = $oInfo; $oMessageCollection->Offset = $oParams->iOffset; $oMessageCollection->Limit = $oParams->iLimit; $oMessageCollection->Search = $sSearch; $oMessageCollection->ThreadUid = $oParams->iThreadUid; $oMessageCollection->Filtered = '' !== \MailSo\Config::$MessageListPermanentFilter; + $oMessageCollection->MessageCount = $iMessageRealCount; + $oMessageCollection->MessageUnseenCount = $iMessageUnseenCount; $aUids = array(); $aAllThreads = []; @@ -934,8 +933,6 @@ class MailClient } } - $oMessageCollection->MessageCount = $iMessageRealCount; - $oMessageCollection->MessageUnseenCount = $iMessageUnseenCount; $oMessageCollection->MessageResultCount = \count($aUids); if (\count($aUids)) @@ -954,9 +951,6 @@ class MailClient ', limit:'.\MailSo\Config::$MessageListCountLimitTrigger.')'); } - $oMessageCollection->MessageCount = $iMessageRealCount; - $oMessageCollection->MessageUnseenCount = $iMessageUnseenCount; - if (\strlen($sSearch)) { $aUids = $this->GetUids($oParams->oCacher, $sSearch, diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageCollection.php index 428e80baa..db607939a 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageCollection.php @@ -67,6 +67,9 @@ class MessageCollection extends \MailSo\Base\Collection */ public $ThreadUid = 0; + // MailSo\Imap\FolderInformation + public $FolderInfo = null; + /** * @var array */ @@ -97,6 +100,7 @@ class MessageCollection extends \MailSo\Base\Collection 'MessageResultCount' => $this->MessageResultCount, 'Folder' => $this->FolderName, 'FolderHash' => $this->FolderHash, + 'FolderInfo' => $this->FolderInfo, 'UidNext' => $this->UidNext, 'ThreadUid' => $this->ThreadUid, 'NewMessages' => $this->NewMessages, diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/SmtpClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/SmtpClient.php index 22ec5ff61..1e3097e63 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/SmtpClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/SmtpClient.php @@ -120,7 +120,9 @@ class SmtpClient extends \MailSo\Net\NetClient $sPassword = $aCredentials['Password']; $type = ''; - $aCredentials['SASLMechanisms'][] = 'LOGIN'; + // https://github.com/the-djmaze/snappymail/pull/423 +// $aCredentials['SASLMechanisms'][] = 'LOGIN'; + \array_unshift($aCredentials['SASLMechanisms'], 'LOGIN'); foreach ($aCredentials['SASLMechanisms'] as $sasl_type) { if ($this->IsAuthSupported($sasl_type) && \SnappyMail\SASL::isSupported($sasl_type)) { $type = $sasl_type; diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php index 59889fdc3..be99c3c54 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php @@ -426,13 +426,12 @@ trait Folders ); $aInboxInformation['Flags'] = $aInboxInformation['MessagesFlags']; unset($aInboxInformation['MessagesFlags']); + return $this->DefaultResponse(__FUNCTION__, $aInboxInformation); } catch (\Throwable $oException) { throw new ClientException(Notifications::MailServerError, $oException); } - - return $this->DefaultResponse(__FUNCTION__, $aInboxInformation); } /** diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php index 83989d264..97f69a264 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php @@ -187,16 +187,10 @@ trait Messages { case 'reply': case 'reply-all': - $this->MailClient()->MessageSetFlag($sDraftInfoFolder, new SequenceSet($iDraftInfoUid), - MessageFlag::ANSWERED); + $this->MailClient()->MessageSetFlag($sDraftInfoFolder, new SequenceSet($iDraftInfoUid), MessageFlag::ANSWERED); break; case 'forward': - $sForwardedFlag = $this->Config()->Get('labs', 'imap_forwarded_flag', ''); - if (\strlen($sForwardedFlag)) - { - $this->MailClient()->MessageSetFlag($sDraftInfoFolder, new SequenceSet($iDraftInfoUid), - $sForwardedFlag); - } + $this->MailClient()->MessageSetFlag($sDraftInfoFolder, new SequenceSet($iDraftInfoUid), MessageFlag::FORWARDED); break; } } @@ -351,22 +345,18 @@ trait Messages $mResult = true; - $sReadReceiptFlag = $this->Config()->Get('labs', 'imap_read_receipt_flag', ''); - if (!empty($sReadReceiptFlag)) + $sFolderFullName = $this->GetActionParam('MessageFolder', ''); + $iUid = (int) $this->GetActionParam('MessageUid', 0); + + $this->Cacher($oAccount)->Set(\RainLoop\KeyPathHelper::ReadReceiptCache($oAccount->Email(), $sFolderFullName, $iUid), '1'); + + if (\strlen($sFolderFullName) && 0 < $iUid) { - $sFolderFullName = $this->GetActionParam('MessageFolder', ''); - $iUid = (int) $this->GetActionParam('MessageUid', 0); - - $this->Cacher($oAccount)->Set(\RainLoop\KeyPathHelper::ReadReceiptCache($oAccount->Email(), $sFolderFullName, $iUid), '1'); - - if (\strlen($sFolderFullName) && 0 < $iUid) + try { - try - { - $this->MailClient()->MessageSetFlag($sFolderFullName, new SequenceSet($iUid), $sReadReceiptFlag, true, true); - } - catch (\Throwable $oException) {} + $this->MailClient()->MessageSetFlag($sFolderFullName, new SequenceSet($iUid), MessageFlag::MDNSENT, true, true); } + catch (\Throwable $oException) {} } } } @@ -523,11 +513,10 @@ trait Messages $sFromFolder = $this->GetActionParam('FromFolder', ''); $sToFolder = $this->GetActionParam('ToFolder', ''); - $bMarkAsRead = !empty($this->GetActionParam('MarkAsRead', '0')); $oUids = new SequenceSet(\explode(',', (string) $this->GetActionParam('Uids', ''))); - if ($bMarkAsRead) + if (!empty($this->GetActionParam('MarkAsRead', '0'))) { try { @@ -539,6 +528,25 @@ trait Messages } } + $sLearning = $this->GetActionParam('Learning', ''); + if ($sLearning) + { + try + { + if ('SPAM' === $sLearning) { + $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::JUNK); + $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::NOTJUNK, false); + } else if ('HAM' === $sLearning) { + $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::NOTJUNK); + $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::JUNK, false); + } + } + catch (\Throwable $oException) + { + unset($oException); + } + } + try { $this->MailClient()->MessageMove($sFromFolder, $sToFolder, $oUids, diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php index 098664c4c..cbd71f682 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php @@ -194,10 +194,12 @@ trait Response 'FileName' => (\strlen($sSubject) ? \MailSo\Base\Utils::ClearXss($sSubject) : 'message-'.$mResult['Uid']) . '.eml' )); - $sForwardedFlag = \strtolower($this->Config()->Get('labs', 'imap_forwarded_flag', '')); - $sReadReceiptFlag = \strtolower($this->Config()->Get('labs', 'imap_read_receipt_flag', '')); - \strlen($sForwardedFlag) && \in_array($sForwardedFlag, $mResult['Flags']) && \array_push($mResult['Flags'], '$forwarded'); - \strlen($sReadReceiptFlag) && \in_array($sReadReceiptFlag, $mResult['Flags']) && \array_push($mResult['Flags'], '$mdnsent'); + // https://datatracker.ietf.org/doc/html/rfc5788#section-3.4.1 + $key = \array_search('$readreceipt', $mResult['Flags']); + if (false !== $key) { + $mResult['Flags'][$key] = '$mdnsent'; + } + $mResult['Flags'] = \array_unique($mResult['Flags']); if ('Message' === $sParent) @@ -220,6 +222,7 @@ trait Response if (\strlen($mResult['ReadReceipt']) && !\in_array('$forwarded', $mResult['Flags'])) { + // \in_array('$mdnsent', $mResult['Flags']) if (\strlen($mResult['ReadReceipt'])) { try diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/AbstractConfig.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/AbstractConfig.php index c2a9402e0..8dfa1dc1b 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/AbstractConfig.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/AbstractConfig.php @@ -187,7 +187,7 @@ abstract class AbstractConfig implements \JsonSerializable } $aData = \parse_ini_file($this->sFile, true); - if ($aSubData && \count($aSubData)) + if ($aData && \count($aData)) { foreach ($aData as $sSectionKey => $aSectionValue) { diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php index 0209bd3fc..94b0a71ce 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php @@ -355,8 +355,6 @@ Enables caching in the system'), 'use_imap_thread' => array(true), 'use_imap_move' => array(false), 'use_imap_expunge_all_on_delete' => array(false), - 'imap_forwarded_flag' => array('$Forwarded'), - 'imap_read_receipt_flag' => array('$ReadReceipt'), 'imap_body_text_limit' => array(555000), 'imap_message_list_fast_simple_search' => array(true), 'imap_message_list_count_limit_trigger' => array(0), diff --git a/snappymail/v/0.0.0/app/templates/Views/User/Login.html b/snappymail/v/0.0.0/app/templates/Views/User/Login.html index 53a04ce36..739f4e4ba 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/Login.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/Login.html @@ -24,12 +24,7 @@ data-i18n="[placeholder]GLOBAL/PASSWORD">
-
- -
-
+
+
diff --git a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html index 7d35eb900..dce497560 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html @@ -133,11 +133,7 @@
-
- - - -
+
diff --git a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html index 76c68a2e2..4e2548800 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html @@ -25,10 +25,7 @@
-
- - -
+
@@ -63,10 +60,10 @@
  • - +
  • - +
  • @@ -216,8 +213,7 @@ data-icon="🖼" data-i18n="MESSAGE/BUTTON_SHOW_IMAGES">
    -
    +
    • @@ -244,7 +240,7 @@ click: function () { checked(!checked()); return false }">
    - +
    - - ×
    diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html index 9f17dd1c5..c5ec43f3e 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html @@ -66,7 +66,10 @@ + + +