mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-06 16:07:03 +03:00
Merge branch 'master' into addressbook
This commit is contained in:
commit
84c1fb5402
60 changed files with 574 additions and 629 deletions
|
|
@ -259,8 +259,6 @@ use_imap_force_selection = Off
|
||||||
use_imap_thread = On
|
use_imap_thread = On
|
||||||
use_imap_move = Off
|
use_imap_move = Off
|
||||||
use_imap_expunge_all_on_delete = Off
|
use_imap_expunge_all_on_delete = Off
|
||||||
imap_forwarded_flag = "$Forwarded"
|
|
||||||
imap_read_receipt_flag = "$ReadReceipt"
|
|
||||||
imap_body_text_limit = 555000
|
imap_body_text_limit = 555000
|
||||||
imap_message_list_fast_simple_search = On
|
imap_message_list_fast_simple_search = On
|
||||||
imap_message_list_count_limit_trigger = 0
|
imap_message_list_count_limit_trigger = 0
|
||||||
|
|
@ -307,5 +305,5 @@ dev_email = ""
|
||||||
dev_password = ""
|
dev_password = ""
|
||||||
|
|
||||||
[version]
|
[version]
|
||||||
current = "2.16.1"
|
current = "2.16.3"
|
||||||
saved = "Fri, 04 Mar 2022 08:55:26 +0000"
|
saved = "Fri, 04 Mar 2022 08:55:26 +0000"
|
||||||
|
|
|
||||||
24
README.md
24
README.md
|
|
@ -140,26 +140,26 @@ RainLoop 1.15 vs SnappyMail
|
||||||
|
|
||||||
|js/* |RainLoop |Snappy |
|
|js/* |RainLoop |Snappy |
|
||||||
|--------------- |--------: |--------: |
|
|--------------- |--------: |--------: |
|
||||||
|admin.js |2.158.025 | 80.335 |
|
|admin.js |2.158.025 | 80.521 |
|
||||||
|app.js |4.215.733 | 405.051 |
|
|app.js |4.215.733 | 405.835 |
|
||||||
|boot.js | 672.433 | 2.050 |
|
|boot.js | 672.433 | 2.050 |
|
||||||
|libs.js | 647.679 | 197.160 |
|
|libs.js | 647.679 | 197.160 |
|
||||||
|sieve.js | 0 | 86.504 |
|
|sieve.js | 0 | 86.362 |
|
||||||
|polyfills.js | 325.908 | 0 |
|
|polyfills.js | 325.908 | 0 |
|
||||||
|serviceworker.js | 0 | 285 |
|
|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 |
|
|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 |
|
|admin.min.js | 255.514 | 40.289 | 73.899 | 13.383 | 60.674 | 11.971 |
|
||||||
|app.min.js | 516.000 | 191.792 |140.430 | 61.896 |110.657 | 53.154 |
|
|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 |
|
|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 |
|
|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 |
|
|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 |1.189.690 | 288.551 |351.061 | 97.933 |292.627 | 85.370 |
|
||||||
|TOTAL user+sieve |1.189.690 | 330.301 |351.061 |108.374 |292.627 | 94.756 |
|
|TOTAL user+sieve |1.189.690 | 330.642 |351.061 |108.476 |292.627 | 94.881 |
|
||||||
|TOTAL admin | 929.204 | 136.548 |285.047 | 49.286 |242.644 | 44.055 |
|
|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.
|
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 |
|
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||||
|------------ |-------: |------: |------: |------: |--------: |
|
|------------ |-------: |------: |------: |------: |--------: |
|
||||||
|app.css | 340.334 | 80.643 | 46.959 | 16.709 | 14.386 |
|
|app.css | 340.334 | 80.979 | 46.959 | 16.774 | 14.439 |
|
||||||
|app.min.css | 274.791 | 64.822 | 39.618 | 14.793 | 13.044 |
|
|app.min.css | 274.791 | 65.092 | 39.618 | 14.841 | 13.085 |
|
||||||
|boot.css | | 1.326 | | 664 | 545 |
|
|boot.css | | 1.326 | | 664 | 545 |
|
||||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||||
|admin.css | | 29.828 | | 6.777 | 5.875 |
|
|admin.css | | 29.828 | | 6.777 | 5.875 |
|
||||||
|
|
|
||||||
|
|
@ -97,11 +97,8 @@ export class MessageModel extends AbstractModel {
|
||||||
isImportant: () => MessagePriority.High === this.priority(),
|
isImportant: () => MessagePriority.High === this.priority(),
|
||||||
hasAttachments: () => this.attachments().hasVisible(),
|
hasAttachments: () => this.attachments().hasVisible(),
|
||||||
|
|
||||||
isDeleted: () => this.flags().includes('\\deleted'),
|
isUnseen: () => !this.flags().includes('\\seen'),
|
||||||
isUnseen: () => !this.flags().includes('\\seen') /* || this.flags().includes('\\unseen')*/,
|
|
||||||
isFlagged: () => this.flags().includes('\\flagged'),
|
isFlagged: () => this.flags().includes('\\flagged'),
|
||||||
isAnswered: () => this.flags().includes('\\answered'),
|
|
||||||
isForwarded: () => this.flags().includes('$forwarded'),
|
|
||||||
isReadReceipt: () => this.flags().includes('$mdnsent')
|
isReadReceipt: () => this.flags().includes('$mdnsent')
|
||||||
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
|
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
|
||||||
// isPhishing: () => this.flags().includes('$phishing')
|
// isPhishing: () => this.flags().includes('$phishing')
|
||||||
|
|
@ -291,13 +288,9 @@ export class MessageModel extends AbstractModel {
|
||||||
let classes = [];
|
let classes = [];
|
||||||
forEachObjectEntry({
|
forEachObjectEntry({
|
||||||
deleted: this.deleted(),
|
deleted: this.deleted(),
|
||||||
'deleted-mark': this.isDeleted(),
|
|
||||||
selected: this.selected(),
|
selected: this.selected(),
|
||||||
checked: this.checked(),
|
checked: this.checked(),
|
||||||
flagged: this.isFlagged(),
|
|
||||||
unseen: this.isUnseen(),
|
unseen: this.isUnseen(),
|
||||||
answered: this.isAnswered(),
|
|
||||||
forwarded: this.isForwarded(),
|
|
||||||
focused: this.focused(),
|
focused: this.focused(),
|
||||||
important: this.isImportant(),
|
important: this.isImportant(),
|
||||||
withAttachments: !!this.attachments().length,
|
withAttachments: !!this.attachments().length,
|
||||||
|
|
@ -306,9 +299,18 @@ export class MessageModel extends AbstractModel {
|
||||||
hasUnseenSubMessage: this.hasUnseenSubMessage(),
|
hasUnseenSubMessage: this.hasUnseenSubMessage(),
|
||||||
hasFlaggedSubMessage: this.hasFlaggedSubMessage()
|
hasFlaggedSubMessage: this.hasFlaggedSubMessage()
|
||||||
}, (key, value) => value && classes.push(key));
|
}, (key, value) => value && classes.push(key));
|
||||||
|
this.flags().forEach(value => classes.push('flag-'+value));
|
||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
* https://datatracker.ietf.org/doc/html/rfc5788
|
||||||
|
*/
|
||||||
|
keywords() {
|
||||||
|
return this.flags().filter(value => '\\' !== value[0]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
|
|
@ -567,19 +569,4 @@ export class MessageModel extends AbstractModel {
|
||||||
return result.html || plainToHtml(this.plain());
|
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(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -332,11 +332,7 @@ MessagelistUserStore.removeMessagesFromList = (
|
||||||
? messageList.filter(item => item && uidForRemove.includes(pInt(item.uid)))
|
? messageList.filter(item => item && uidForRemove.includes(pInt(item.uid)))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
messages.forEach(item => {
|
messages.forEach(item => item && item.isUnseen() && ++unseenCount);
|
||||||
if (item && item.isUnseen()) {
|
|
||||||
++unseenCount;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fromFolder && !copy) {
|
if (fromFolder && !copy) {
|
||||||
fromFolder.messageCountAll(
|
fromFolder.messageCountAll(
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,6 @@
|
||||||
opacity: 0.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 {
|
.checkboxAttachment {
|
||||||
bottom: 6px;
|
bottom: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ html:not(rl-mobile) {
|
||||||
margin-right:5px
|
margin-right:5px
|
||||||
}
|
}
|
||||||
|
|
||||||
&.deleted-mark {
|
&.flag-\\deleted {
|
||||||
opacity: .7;
|
opacity: .7;
|
||||||
.subjectParent {
|
.subjectParent {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
|
|
@ -228,7 +228,7 @@ html:not(rl-mobile) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkboxMessage {
|
.checkboxMessage {
|
||||||
padding: 0 6px;
|
margin: 0 6px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,8 +242,6 @@ html:not(rl-mobile) {
|
||||||
.attachmentParent {
|
.attachmentParent {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 2px 10px 0 5px;
|
margin: 2px 10px 0 5px;
|
||||||
color: #666;
|
|
||||||
text-shadow: 0 1px 0 #eee;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.senderParent, .subjectParent {
|
.senderParent, .subjectParent {
|
||||||
|
|
@ -340,15 +338,15 @@ html:not(rl-mobile) {
|
||||||
.flagParent {
|
.flagParent {
|
||||||
padding: 0 10px 0 5px;
|
padding: 0 10px 0 5px;
|
||||||
}
|
}
|
||||||
&.flagged .flagParent::after,
|
&.flag-\\flagged .flagParent::after,
|
||||||
&.hasFlaggedSubMessage .flagParent::after {
|
&.hasFlaggedSubMessage .flagParent::after {
|
||||||
color: orange;
|
color: orange;
|
||||||
content: '★'; /*⚑*/
|
content: '★'; /*⚑*/
|
||||||
}
|
}
|
||||||
&:not(.flagged):not(.hasFlaggedSubMessage) .flagParent::after {
|
&:not(.flag-\\flagged):not(.hasFlaggedSubMessage) .flagParent::after {
|
||||||
content: '☆'; /*⚐*/
|
content: '☆'; /*⚐*/
|
||||||
}
|
}
|
||||||
&:not(.flagged):not(.hasFlaggedSubMessage) .flagParent:not(:hover) {
|
&:not(.flag-\\flagged):not(.hasFlaggedSubMessage) .flagParent:not(:hover) {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -392,7 +390,7 @@ html.rl-ctrl-key-pressed .messageListItem {
|
||||||
|
|
||||||
.checkboxMessage {
|
.checkboxMessage {
|
||||||
line-height: 12px;
|
line-height: 12px;
|
||||||
margin: 10px 0 -5px;
|
margin: 10px 6px -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subjectParent {
|
.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; }
|
||||||
|
|
|
||||||
|
|
@ -42,13 +42,10 @@ html.rl-no-preview-pane {
|
||||||
line-height: 70px;
|
line-height: 70px;
|
||||||
padding-top: 100px;
|
padding-top: 100px;
|
||||||
color: #999;
|
color: #999;
|
||||||
|
}
|
||||||
.icon-mail {
|
.b-message-view-checked-helper::after {
|
||||||
font-size: 100px;
|
content: ' ✉';
|
||||||
font-size: 50px;
|
font-family: snappymail;
|
||||||
line-height: 90px;
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.b-message-view-desc {
|
.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 {
|
.controls-handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 5px;
|
bottom: 5px;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { messagesDeleteHelper } from 'Common/Folders';
|
||||||
import { serverRequest } from 'Common/Links';
|
import { serverRequest } from 'Common/Links';
|
||||||
import { i18n, getNotification, getUploadErrorDescByCode, timestampToString } from 'Common/Translator';
|
import { i18n, getNotification, getUploadErrorDescByCode, timestampToString } from 'Common/Translator';
|
||||||
import { MessageFlagsCache, setFolderHash } from 'Common/Cache';
|
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 { exitFullscreen, isFullscreen, toggleFullscreen } from 'Common/Fullscreen';
|
||||||
|
|
||||||
import { AppUserStore } from 'Stores/User/App';
|
import { AppUserStore } from 'Stores/User/App';
|
||||||
|
|
@ -236,6 +236,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
this.sLastFocusedField = 'to';
|
this.sLastFocusedField = 'to';
|
||||||
|
|
||||||
this.allowContacts = AppUserStore.allowContacts();
|
this.allowContacts = AppUserStore.allowContacts();
|
||||||
|
this.allowIdentities = SettingsCapa('Identities');
|
||||||
|
|
||||||
this.bSkipNextHide = false;
|
this.bSkipNextHide = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -571,21 +571,12 @@ export class MailMessageList extends AbstractViewRight {
|
||||||
|
|
||||||
seenMessagesFast(seen) {
|
seenMessagesFast(seen) {
|
||||||
const checked = MessagelistUserStore.listCheckedOrSelected();
|
const checked = MessagelistUserStore.listCheckedOrSelected();
|
||||||
if (checked.length) {
|
if (checked.length && null != seen) {
|
||||||
if (undefined === seen) {
|
listAction(
|
||||||
const unseen = checked.filter(message => message.isUnseen());
|
checked[0].folder,
|
||||||
listAction(
|
seen ? MessageSetAction.SetSeen : MessageSetAction.UnsetSeen,
|
||||||
checked[0].folder,
|
checked
|
||||||
unseen.length ? MessageSetAction.SetSeen : MessageSetAction.UnsetSeen,
|
);
|
||||||
checked
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
listAction(
|
|
||||||
checked[0].folder,
|
|
||||||
seen ? MessageSetAction.SetSeen : MessageSetAction.UnsetSeen,
|
|
||||||
checked
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,9 +123,6 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
this.fullScreenMode = isFullscreen;
|
this.fullScreenMode = isFullscreen;
|
||||||
this.toggleFullScreen = toggleFullscreen;
|
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.downloadAsZipError = ko.observable(false).extend({ falseTimeout: 7000 });
|
||||||
|
|
||||||
this.messageDomFocused = ko.observable(false).extend({ rateLimit: 0 });
|
this.messageDomFocused = ko.observable(false).extend({ rateLimit: 0 });
|
||||||
|
|
@ -136,7 +133,10 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
this.addComputables({
|
this.addComputables({
|
||||||
allowAttachmentControls: () => arrayLength(attachmentsActions) && SettingsCapa('AttachmentsActions'),
|
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: {
|
lastReplyAction: {
|
||||||
read: this.lastReplyAction_,
|
read: this.lastReplyAction_,
|
||||||
|
|
@ -538,8 +538,6 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => this.downloadAsZipError(true));
|
.catch(() => this.downloadAsZipError(true));
|
||||||
} else {
|
|
||||||
this.highlightUnselectedAttachments(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -566,20 +564,20 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
readReceipt() {
|
readReceipt() {
|
||||||
let oMessage = currentMessage()
|
let oMessage = currentMessage()
|
||||||
if (oMessage.readReceipt()) {
|
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,
|
MessageFolder: oMessage.folder,
|
||||||
MessageUid: oMessage.uid,
|
MessageUid: oMessage.uid,
|
||||||
ReadReceipt: oMessage.readReceipt(),
|
ReadReceipt: oMessage.readReceipt(),
|
||||||
Subject: i18n('READ_RECEIPT/SUBJECT', { SUBJECT: oMessage.subject() }),
|
Subject: i18n('READ_RECEIPT/SUBJECT', { SUBJECT: oMessage.subject() }),
|
||||||
Text: i18n('READ_RECEIPT/BODY', { 'READ-RECEIPT': AccountUserStore.email() })
|
Text: i18n('READ_RECEIPT/BODY', { 'READ-RECEIPT': AccountUserStore.email() })
|
||||||
});
|
});
|
||||||
|
|
||||||
oMessage.flags.push('$mdnsent');
|
|
||||||
// oMessage.flags.valueHasMutated();
|
|
||||||
|
|
||||||
MessageFlagsCache.store(oMessage);
|
|
||||||
|
|
||||||
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
This app packages SnappyMail <upstream>2.16.1</upstream>.
|
This app packages SnappyMail <upstream>2.16.3</upstream>.
|
||||||
|
|
||||||
SnappyMail is a simple, modern, lightweight & fast web-based email client.
|
SnappyMail is a simple, modern, lightweight & fast web-based email client.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ RUN mkdir -p /app/code
|
||||||
WORKDIR /app/code
|
WORKDIR /app/code
|
||||||
|
|
||||||
# If you change the extraction below, be sure to test on scaleway
|
# 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 && \
|
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 && \
|
unzip /tmp/snappymail.zip -d /app/code && \
|
||||||
rm /tmp/snappymail.zip && \
|
rm /tmp/snappymail.zip && \
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
2.16.1
|
2.16.3
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<name>SnappyMail</name>
|
<name>SnappyMail</name>
|
||||||
<summary>SnappyMail Webmail</summary>
|
<summary>SnappyMail Webmail</summary>
|
||||||
<description>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).</description>
|
<description>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).</description>
|
||||||
<version>2.16.1</version>
|
<version>2.16.3</version>
|
||||||
<licence>agpl</licence>
|
<licence>agpl</licence>
|
||||||
<author>SnappyMail Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
|
<author>SnappyMail Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
|
||||||
<namespace>SnappyMail</namespace>
|
<namespace>SnappyMail</namespace>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
|
||||||
# script_snappymail_versions()
|
# script_snappymail_versions()
|
||||||
sub script_snappymail_versions
|
sub script_snappymail_versions
|
||||||
{
|
{
|
||||||
return ( "2.16.1" );
|
return ( "2.16.3" );
|
||||||
}
|
}
|
||||||
|
|
||||||
sub script_snappymail_version_desc
|
sub script_snappymail_version_desc
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"title": "SnappyMail",
|
"title": "SnappyMail",
|
||||||
"description": "Simple, modern & fast web-based email client",
|
"description": "Simple, modern & fast web-based email client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.16.1",
|
"version": "2.16.3",
|
||||||
"homepage": "https://snappymail.eu",
|
"homepage": "https://snappymail.eu",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "DJ Maze",
|
"name": "DJ Maze",
|
||||||
|
|
|
||||||
107
plugins/change-password-froxlor/driver.php
Normal file
107
plugins/change-password-froxlor/driver.php
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class ChangePasswordFroxlorDriver
|
||||||
|
{
|
||||||
|
const
|
||||||
|
NAME = 'Froxlor',
|
||||||
|
DESCRIPTION = 'Change passwords in Froxlor.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \RainLoop\Config\Plugin
|
||||||
|
*/
|
||||||
|
private $oConfig = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \MailSo\Log\Logger
|
||||||
|
*/
|
||||||
|
private $oLogger = null;
|
||||||
|
|
||||||
|
function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger)
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
plugins/change-password-froxlor/index.php
Normal file
20
plugins/change-password-froxlor/index.php
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use \RainLoop\Exceptions\ClientException;
|
||||||
|
|
||||||
|
class ChangePasswordPoppassdPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
{
|
||||||
|
const
|
||||||
|
NAME = 'Change Password Froxlor',
|
||||||
|
AUTHOR = 'Euphonique',
|
||||||
|
VERSION = '1.0',
|
||||||
|
RELEASE = '2022-05-30',
|
||||||
|
REQUIRED = '2.15.3',
|
||||||
|
CATEGORY = 'Security',
|
||||||
|
DESCRIPTION = 'Extension to allow users to change their passwords through Froxlor';
|
||||||
|
|
||||||
|
public function Supported() : string
|
||||||
|
{
|
||||||
|
return 'Use Change Password plugin';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,22 +9,21 @@ class ChangeSmtpEhloMessagePlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
|
||||||
public function Init() : void
|
public function Init() : void
|
||||||
{
|
{
|
||||||
$this->addHook('smtp.credentials', 'FilterSmtpCredentials');
|
$this->addHook('smtp.before-connect', 'FilterSmtpCredentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param \RainLoop\Model\Account $oAccount
|
* @param \RainLoop\Model\Account $oAccount
|
||||||
* @param array $aSmtpCredentials
|
* @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();
|
||||||
// Default:
|
//
|
||||||
// $aSmtpCredentials['Ehlo'] = \MailSo\Smtp\SmtpClient::EhloHelper();
|
// or write your custom php
|
||||||
//
|
$aSmtpCredentials['Ehlo'] = 'localhost';
|
||||||
// or write your custom php
|
|
||||||
$aSmtpCredentials['Ehlo'] = 'localhost';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
class ContactsExampleSuggestions implements \RainLoop\Providers\Suggestions\ISuggestions
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param \RainLoop\Model\Account $oAccount
|
|
||||||
* @param string $sQuery
|
|
||||||
* @param int $iLimit = 20
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function Process(RainLoop\Model\Account $oAccount, string $sQuery, int $iLimit = 20): array
|
|
||||||
{
|
|
||||||
$aResult = array(
|
|
||||||
array($oAccount->Email(), ''),
|
|
||||||
array('email@domain.com', 'name')
|
|
||||||
);
|
|
||||||
|
|
||||||
return $aResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
class ContactsSuggestionsExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
|
|
||||||
{
|
|
||||||
const
|
|
||||||
NAME = 'Custom Contacts Suggestions Example',
|
|
||||||
CATEGORY = 'General',
|
|
||||||
DESCRIPTION = 'Custom contacts suggestions example';
|
|
||||||
|
|
||||||
public function Init() : void
|
|
||||||
{
|
|
||||||
$this->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
class CustomAdminSettingsTabPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|
||||||
{
|
|
||||||
const
|
|
||||||
NAME = 'Custom Admin Settings',
|
|
||||||
CATEGORY = 'General',
|
|
||||||
DESCRIPTION = 'Custom admin settings example';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function Init() : void
|
|
||||||
{
|
|
||||||
$this->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()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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');
|
|
||||||
|
|
||||||
}());
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
[SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN]
|
|
||||||
TAB_NAME = "Плагин"
|
|
||||||
LEGEND_CUSTOM = "Плагин (Пример)"
|
|
||||||
LABEL_PHP_VERSION = "PHP версия"
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
class CustomAuthExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
|
|
||||||
{
|
|
||||||
const
|
|
||||||
NAME = 'Custom Auth Example Extension',
|
|
||||||
VERSION = '2.1',
|
|
||||||
REQUIRED = '2.5.0',
|
|
||||||
CATEGORY = 'Login',
|
|
||||||
DESCRIPTION = 'Custom auth mechanism example';
|
|
||||||
|
|
||||||
public function Init() : void
|
|
||||||
{
|
|
||||||
$this->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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
class CustomSettingsTabPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|
||||||
{
|
|
||||||
const
|
|
||||||
NAME = 'Custom Settings Tab',
|
|
||||||
CATEGORY = 'General',
|
|
||||||
DESCRIPTION = 'Example extension for custom settings tabs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function Init() : void
|
|
||||||
{
|
|
||||||
$this->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
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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');
|
|
||||||
|
|
||||||
}());
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
[SETTINGS_CUSTOM_PLUGIN]
|
|
||||||
TAB_NAME = "Плагин"
|
|
||||||
LEGEND_CUSTOM = "Плагин (Пример)"
|
|
||||||
LABEL_SKYPE = "Skype"
|
|
||||||
LABEL_FACEBOOK = "Facebook"
|
|
||||||
BUTTON_SAVE = "Сохранить"
|
|
||||||
|
|
@ -31,6 +31,22 @@ class ExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
$this->addTemplate(string $sFile, bool $bAdminScope = false);
|
$this->addTemplate(string $sFile, bool $bAdminScope = false);
|
||||||
$this->addTemplateHook(string $sName, string $sPlace, string $sLocalTemplateName, bool $bPrepend = 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 Config() : \RainLoop\Config\Plugin
|
||||||
public function Description() : string
|
public function Description() : string
|
||||||
|
|
|
||||||
37
plugins/example/js/ExampleAdminSettings.js
Normal file
37
plugins/example/js/ExampleAdminSettings.js
Normal file
|
|
@ -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);
|
||||||
68
plugins/example/js/ExampleUserSettings.js
Normal file
68
plugins/example/js/ExampleUserSettings.js
Normal file
|
|
@ -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);
|
||||||
15
plugins/example/langs/en.json
Normal file
15
plugins/example/langs/en.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="form-horizontal">
|
<div class="form-horizontal">
|
||||||
<div class="legend">
|
<div class="legend">
|
||||||
<span class="i18n" data-i18n="SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN/LEGEND_CUSTOM"></span>
|
<span class="i18n" data-i18n="SETTINGS_EXAMPLE_ADMIN_EXAMPLE_TAB_PLUGIN/LEGEND_EXAMPLE"></span>
|
||||||
|
|
||||||
<i class="icon-spinner animated" style="margin-top: 5px" data-bind="visible: loading"></i>
|
<i class="icon-spinner animated" style="margin-top: 5px" data-bind="visible: loading"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label class="control-label">
|
<label class="control-label">
|
||||||
<span class="i18n" data-i18n="SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN/LABEL_PHP_VERSION"></span>
|
<span class="i18n" data-i18n="SETTINGS_EXAMPLE_ADMIN_EXAMPLE_TAB_PLUGIN/LABEL_PHP_VERSION"></span>
|
||||||
</label>
|
</label>
|
||||||
<div class="controls" style="padding-top: 5px">
|
<div class="controls" style="padding-top: 5px">
|
||||||
<b data-bind="text: php"></b>
|
<b data-bind="text: php"></b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="form-horizontal">
|
<div class="form-horizontal">
|
||||||
<div class="legend">
|
<div class="legend">
|
||||||
<span class="i18n" data-i18n="SETTINGS_CUSTOM_PLUGIN/LEGEND_CUSTOM"></span>
|
<span class="i18n" data-i18n="SETTINGS_EXAMPLE_PLUGIN/LEGEND_EXAMPLE"></span>
|
||||||
|
|
||||||
<i class="icon-spinner animated" style="margin-top: 5px" data-bind="visible: loading"></i>
|
<i class="icon-spinner animated" style="margin-top: 5px" data-bind="visible: loading"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label class="control-label">
|
<label class="control-label">
|
||||||
<span class="i18n" data-i18n="SETTINGS_CUSTOM_PLUGIN/LABEL_SKYPE"></span>
|
<span class="i18n" data-i18n="SETTINGS_EXAMPLE_PLUGIN/LABEL_SKYPE"></span>
|
||||||
</label>
|
</label>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<input type="text" data-bind="value: userSkype, enable: !savingOrLoading()" />
|
<input type="text" data-bind="value: userSkype, enable: !savingOrLoading()" />
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label class="control-label">
|
<label class="control-label">
|
||||||
<span class="i18n" data-i18n="SETTINGS_CUSTOM_PLUGIN/LABEL_FACEBOOK"></span>
|
<span class="i18n" data-i18n="SETTINGS_EXAMPLE_PLUGIN/LABEL_FACEBOOK"></span>
|
||||||
</label>
|
</label>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<input type="text" data-bind="value: userFacebook, enable: !savingOrLoading()" />
|
<input type="text" data-bind="value: userFacebook, enable: !savingOrLoading()" />
|
||||||
|
|
@ -23,10 +23,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="btn" data-bind="click: customJsonSaveData, enable: !savingOrLoading()">
|
<button class="btn" data-bind="click: exampleJsonSaveData, enable: !savingOrLoading()">
|
||||||
<i data-bind="css: {'icon-floppy': !saving(), 'icon-spinner animated': saving()}" class="icon-floppy"></i>
|
<i data-bind="css: {'icon-floppy': !saving(), 'icon-spinner animated': saving()}" class="icon-floppy"></i>
|
||||||
|
|
||||||
<span class="i18n" data-i18n="SETTINGS_CUSTOM_PLUGIN/BUTTON_SAVE"></span>
|
<span class="i18n" data-i18n="SETTINGS_EXAMPLE_PLUGIN/BUTTON_SAVE"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -402,12 +402,12 @@ trait Messages
|
||||||
* @throws \MailSo\Net\Exceptions\Exception
|
* @throws \MailSo\Net\Exceptions\Exception
|
||||||
* @throws \MailSo\Imap\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 = new \MailSo\Imap\Requests\THREAD($this);
|
||||||
$oThread->sCriterias = $sSearchCriterias;
|
$oThread->sCriterias = $sSearchCriterias;
|
||||||
$oThread->bUid = $bReturnUid;
|
$oThread->bUid = $bReturnUid;
|
||||||
return $oThread->SendRequestGetResponse();
|
yield from $oThread->SendRequestIterateResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getSimpleESearchOrESortResult(bool $bReturnUid) : array
|
private function getSimpleESearchOrESortResult(bool $bReturnUid) : array
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,7 @@ trait Quota
|
||||||
*/
|
*/
|
||||||
public function Quota(string $sRootName = '') : ?array
|
public function Quota(string $sRootName = '') : ?array
|
||||||
{
|
{
|
||||||
return $this->IsSupported('QUOTA')
|
return $this->getQuota(false, $sRootName);
|
||||||
? $this->getQuotaResult($this->SendRequestGetResponse("GETQUOTA {$this->EscapeFolderName($sRootName)}"))
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -41,18 +39,19 @@ trait Quota
|
||||||
*/
|
*/
|
||||||
public function QuotaRoot(string $sFolderName = 'INBOX') : ?array
|
public function QuotaRoot(string $sFolderName = 'INBOX') : ?array
|
||||||
{
|
{
|
||||||
return $this->IsSupported('QUOTA')
|
return $this->getQuota(true, $sFolderName);
|
||||||
? $this->getQuotaResult($this->SendRequestGetResponse("GETQUOTAROOT {$this->EscapeFolderName($sFolderName)}"))
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// * QUOTA "User quota" (STORAGE 1284645 2097152)\r\n
|
// * 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);
|
$aReturn = array(0, 0);
|
||||||
foreach ($oResponseCollection as $oResponse) {
|
foreach ($this->yieldUntaggedResponses() as $oResponse) {
|
||||||
if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType
|
if ('QUOTA' === $oResponse->StatusOrIndex
|
||||||
&& 'QUOTA' === $oResponse->StatusOrIndex
|
|
||||||
&& isset($oResponse->ResponseList[3])
|
&& isset($oResponse->ResponseList[3])
|
||||||
&& \is_array($oResponse->ResponseList[3])
|
&& \is_array($oResponse->ResponseList[3])
|
||||||
&& 2 < \count($oResponse->ResponseList[3])
|
&& 2 < \count($oResponse->ResponseList[3])
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
*
|
*
|
||||||
* For the full copyright and license information, please view the LICENSE
|
* For the full copyright and license information, please view the LICENSE
|
||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
|
*
|
||||||
|
* https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace MailSo\Imap\Enumerations;
|
namespace MailSo\Imap\Enumerations;
|
||||||
|
|
@ -18,10 +20,17 @@ namespace MailSo\Imap\Enumerations;
|
||||||
*/
|
*/
|
||||||
abstract class MessageFlag
|
abstract class MessageFlag
|
||||||
{
|
{
|
||||||
// const RECENT = '\\Recent'; // IMAP4rev2 deprecated
|
const
|
||||||
const SEEN = '\\Seen';
|
// RECENT = '\\Recent', // IMAP4rev2 deprecated
|
||||||
const DELETED = '\\Deleted';
|
SEEN = '\\Seen',
|
||||||
const FLAGGED = '\\Flagged';
|
DELETED = '\\Deleted',
|
||||||
const ANSWERED = '\\Answered';
|
FLAGGED = '\\Flagged',
|
||||||
const DRAFT = '\\Draft';
|
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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ namespace MailSo\Imap\Enumerations;
|
||||||
*/
|
*/
|
||||||
abstract class StoreAction
|
abstract class StoreAction
|
||||||
{
|
{
|
||||||
const SET_FLAGS = 'FLAGS';
|
// const SET_FLAGS = 'FLAGS';
|
||||||
const SET_FLAGS_SILENT = 'FLAGS.SILENT';
|
// const SET_FLAGS_SILENT = 'FLAGS.SILENT';
|
||||||
const ADD_FLAGS = '+FLAGS';
|
const ADD_FLAGS = '+FLAGS';
|
||||||
const ADD_FLAGS_SILENT = '+FLAGS.SILENT';
|
const ADD_FLAGS_SILENT = '+FLAGS.SILENT';
|
||||||
const REMOVE_FLAGS = '-FLAGS';
|
const REMOVE_FLAGS = '-FLAGS';
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ namespace MailSo\Imap;
|
||||||
* @category MailSo
|
* @category MailSo
|
||||||
* @package Imap
|
* @package Imap
|
||||||
*/
|
*/
|
||||||
class FolderInformation
|
class FolderInformation implements \JsonSerializable
|
||||||
{
|
{
|
||||||
use Traits\Status;
|
use Traits\Status;
|
||||||
|
|
||||||
|
|
@ -36,6 +36,7 @@ class FolderInformation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
|
* NOTE: Empty when FolderExamine is used
|
||||||
*/
|
*/
|
||||||
public $PermanentFlags = array();
|
public $PermanentFlags = array();
|
||||||
|
|
||||||
|
|
@ -57,4 +58,23 @@ class FolderInformation
|
||||||
\in_array($sFlag, $this->PermanentFlags) ||
|
\in_array($sFlag, $this->PermanentFlags) ||
|
||||||
\in_array($sFlag, $this->Flags);
|
\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,
|
||||||
|
*/
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ class THREAD extends Request
|
||||||
parent::__construct($oImapClient);
|
parent::__construct($oImapClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function SendRequestGetResponse() : array
|
public function SendRequestIterateResponse() : iterable
|
||||||
{
|
{
|
||||||
if (!$this->oImapClient->IsSupported(\strtoupper("THREAD={$this->sAlgorithm}"))) {
|
if (!$this->oImapClient->IsSupported(\strtoupper("THREAD={$this->sAlgorithm}"))) {
|
||||||
$this->oImapClient->writeLogException(
|
$this->oImapClient->writeLogException(
|
||||||
|
|
@ -58,7 +58,6 @@ class THREAD extends Request
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$aReturn = array();
|
|
||||||
foreach ($this->oImapClient->yieldUntaggedResponses() as $oResponse) {
|
foreach ($this->oImapClient->yieldUntaggedResponses() as $oResponse) {
|
||||||
$iOffset = ($this->bUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && 'THREAD' === $oResponse->ResponseList[2]) ? 1 : 0;
|
$iOffset = ($this->bUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && 'THREAD' === $oResponse->ResponseList[2]) ? 1 : 0;
|
||||||
if (('THREAD' === $oResponse->StatusOrIndex || $iOffset)
|
if (('THREAD' === $oResponse->StatusOrIndex || $iOffset)
|
||||||
|
|
@ -69,12 +68,11 @@ class THREAD extends Request
|
||||||
for ($iIndex = 2 + $iOffset; $iIndex < $iLen; ++$iIndex) {
|
for ($iIndex = 2 + $iOffset; $iIndex < $iLen; ++$iIndex) {
|
||||||
$aNewValue = $this->validateThreadItem($oResponse->ResponseList[$iIndex]);
|
$aNewValue = $this->validateThreadItem($oResponse->ResponseList[$iIndex]);
|
||||||
if (\is_array($aNewValue)) {
|
if (\is_array($aNewValue)) {
|
||||||
$aReturn[] = $aNewValue;
|
yield $aNewValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $aReturn;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,7 @@ class Folder implements \JsonSerializable
|
||||||
'Selectable' => $this->IsSelectable(),
|
'Selectable' => $this->IsSelectable(),
|
||||||
'Flags' => $this->FlagsLowerCase(),
|
'Flags' => $this->FlagsLowerCase(),
|
||||||
// 'Extended' => $aExtended,
|
// 'Extended' => $aExtended,
|
||||||
|
// 'PermanentFlags' => $this->oImapFolder->PermanentFlags,
|
||||||
'Metadata' => $this->oImapFolder->Metadata()
|
'Metadata' => $this->oImapFolder->Metadata()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -521,21 +521,19 @@ class MailClient
|
||||||
*/
|
*/
|
||||||
public function FolderInformation(string $sFolderName, int $iPrevUidNext = 0, SequenceSet $oRange = null) : array
|
public function FolderInformation(string $sFolderName, int $iPrevUidNext = 0, SequenceSet $oRange = null) : array
|
||||||
{
|
{
|
||||||
$aFlags = array();
|
|
||||||
|
|
||||||
list($iCount, $iUnseenCount, $iUidNext, $iHighestModSeq, $iAppendLimit, $sMailboxId) = $this->initFolderValues($sFolderName);
|
list($iCount, $iUnseenCount, $iUidNext, $iHighestModSeq, $iAppendLimit, $sMailboxId) = $this->initFolderValues($sFolderName);
|
||||||
|
|
||||||
if ($oRange && \count($oRange))
|
$aFlags = array();
|
||||||
{
|
if ($oRange && \count($oRange)) {
|
||||||
$this->oImapClient->FolderExamine($sFolderName);
|
$oInfo = $this->oImapClient->FolderExamine($sFolderName);
|
||||||
|
// $oInfo->PermanentFlags
|
||||||
|
|
||||||
$aFetchResponse = $this->oImapClient->Fetch(array(
|
$aFetchResponse = $this->oImapClient->Fetch(array(
|
||||||
FetchType::UID,
|
FetchType::UID,
|
||||||
FetchType::FLAGS
|
FetchType::FLAGS
|
||||||
), (string) $oRange, $oRange->UID);
|
), (string) $oRange, $oRange->UID);
|
||||||
|
|
||||||
foreach ($aFetchResponse as $oFetchResponse)
|
foreach ($aFetchResponse as $oFetchResponse) {
|
||||||
{
|
|
||||||
$iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID);
|
$iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID);
|
||||||
$aLowerFlags = \array_map('strtolower', $oFetchResponse->GetFetchValue(FetchType::FLAGS));
|
$aLowerFlags = \array_map('strtolower', $oFetchResponse->GetFetchValue(FetchType::FLAGS));
|
||||||
$aFlags[] = array(
|
$aFlags[] = array(
|
||||||
|
|
@ -620,10 +618,15 @@ class MailClient
|
||||||
|
|
||||||
$this->oImapClient->FolderExamine($sFolderName);
|
$this->oImapClient->FolderExamine($sFolderName);
|
||||||
|
|
||||||
$aThreadUids = array();
|
$aResult = array();
|
||||||
try
|
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)
|
catch (\MailSo\Imap\Exceptions\RuntimeException $oException)
|
||||||
{
|
{
|
||||||
|
|
@ -631,14 +634,6 @@ class MailClient
|
||||||
unset($oException);
|
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))
|
if ($oCacher && $oCacher->IsInited() && !empty($sSerializedHashKey))
|
||||||
{
|
{
|
||||||
$oCacher->Set($sSerializedHashKey, \json_encode(array(
|
$oCacher->Set($sSerializedHashKey, \json_encode(array(
|
||||||
|
|
@ -830,15 +825,19 @@ class MailClient
|
||||||
|
|
||||||
list($iMessageRealCount, $iMessageUnseenCount, $iUidNext, $iHighestModSeq) = $this->initFolderValues($oParams->sFolderName);
|
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 = new MessageCollection;
|
||||||
$oMessageCollection->FolderName = $oParams->sFolderName;
|
$oMessageCollection->FolderName = $oParams->sFolderName;
|
||||||
|
$oMessageCollection->FolderInfo = $oInfo;
|
||||||
$oMessageCollection->Offset = $oParams->iOffset;
|
$oMessageCollection->Offset = $oParams->iOffset;
|
||||||
$oMessageCollection->Limit = $oParams->iLimit;
|
$oMessageCollection->Limit = $oParams->iLimit;
|
||||||
$oMessageCollection->Search = $sSearch;
|
$oMessageCollection->Search = $sSearch;
|
||||||
$oMessageCollection->ThreadUid = $oParams->iThreadUid;
|
$oMessageCollection->ThreadUid = $oParams->iThreadUid;
|
||||||
$oMessageCollection->Filtered = '' !== \MailSo\Config::$MessageListPermanentFilter;
|
$oMessageCollection->Filtered = '' !== \MailSo\Config::$MessageListPermanentFilter;
|
||||||
|
$oMessageCollection->MessageCount = $iMessageRealCount;
|
||||||
|
$oMessageCollection->MessageUnseenCount = $iMessageUnseenCount;
|
||||||
|
|
||||||
$aUids = array();
|
$aUids = array();
|
||||||
$aAllThreads = [];
|
$aAllThreads = [];
|
||||||
|
|
@ -934,8 +933,6 @@ class MailClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$oMessageCollection->MessageCount = $iMessageRealCount;
|
|
||||||
$oMessageCollection->MessageUnseenCount = $iMessageUnseenCount;
|
|
||||||
$oMessageCollection->MessageResultCount = \count($aUids);
|
$oMessageCollection->MessageResultCount = \count($aUids);
|
||||||
|
|
||||||
if (\count($aUids))
|
if (\count($aUids))
|
||||||
|
|
@ -954,9 +951,6 @@ class MailClient
|
||||||
', limit:'.\MailSo\Config::$MessageListCountLimitTrigger.')');
|
', limit:'.\MailSo\Config::$MessageListCountLimitTrigger.')');
|
||||||
}
|
}
|
||||||
|
|
||||||
$oMessageCollection->MessageCount = $iMessageRealCount;
|
|
||||||
$oMessageCollection->MessageUnseenCount = $iMessageUnseenCount;
|
|
||||||
|
|
||||||
if (\strlen($sSearch))
|
if (\strlen($sSearch))
|
||||||
{
|
{
|
||||||
$aUids = $this->GetUids($oParams->oCacher, $sSearch,
|
$aUids = $this->GetUids($oParams->oCacher, $sSearch,
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,9 @@ class MessageCollection extends \MailSo\Base\Collection
|
||||||
*/
|
*/
|
||||||
public $ThreadUid = 0;
|
public $ThreadUid = 0;
|
||||||
|
|
||||||
|
// MailSo\Imap\FolderInformation
|
||||||
|
public $FolderInfo = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
|
|
@ -97,6 +100,7 @@ class MessageCollection extends \MailSo\Base\Collection
|
||||||
'MessageResultCount' => $this->MessageResultCount,
|
'MessageResultCount' => $this->MessageResultCount,
|
||||||
'Folder' => $this->FolderName,
|
'Folder' => $this->FolderName,
|
||||||
'FolderHash' => $this->FolderHash,
|
'FolderHash' => $this->FolderHash,
|
||||||
|
'FolderInfo' => $this->FolderInfo,
|
||||||
'UidNext' => $this->UidNext,
|
'UidNext' => $this->UidNext,
|
||||||
'ThreadUid' => $this->ThreadUid,
|
'ThreadUid' => $this->ThreadUid,
|
||||||
'NewMessages' => $this->NewMessages,
|
'NewMessages' => $this->NewMessages,
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,9 @@ class SmtpClient extends \MailSo\Net\NetClient
|
||||||
$sPassword = $aCredentials['Password'];
|
$sPassword = $aCredentials['Password'];
|
||||||
|
|
||||||
$type = '';
|
$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) {
|
foreach ($aCredentials['SASLMechanisms'] as $sasl_type) {
|
||||||
if ($this->IsAuthSupported($sasl_type) && \SnappyMail\SASL::isSupported($sasl_type)) {
|
if ($this->IsAuthSupported($sasl_type) && \SnappyMail\SASL::isSupported($sasl_type)) {
|
||||||
$type = $sasl_type;
|
$type = $sasl_type;
|
||||||
|
|
|
||||||
|
|
@ -426,13 +426,12 @@ trait Folders
|
||||||
);
|
);
|
||||||
$aInboxInformation['Flags'] = $aInboxInformation['MessagesFlags'];
|
$aInboxInformation['Flags'] = $aInboxInformation['MessagesFlags'];
|
||||||
unset($aInboxInformation['MessagesFlags']);
|
unset($aInboxInformation['MessagesFlags']);
|
||||||
|
return $this->DefaultResponse(__FUNCTION__, $aInboxInformation);
|
||||||
}
|
}
|
||||||
catch (\Throwable $oException)
|
catch (\Throwable $oException)
|
||||||
{
|
{
|
||||||
throw new ClientException(Notifications::MailServerError, $oException);
|
throw new ClientException(Notifications::MailServerError, $oException);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->DefaultResponse(__FUNCTION__, $aInboxInformation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -187,16 +187,10 @@ trait Messages
|
||||||
{
|
{
|
||||||
case 'reply':
|
case 'reply':
|
||||||
case 'reply-all':
|
case 'reply-all':
|
||||||
$this->MailClient()->MessageSetFlag($sDraftInfoFolder, new SequenceSet($iDraftInfoUid),
|
$this->MailClient()->MessageSetFlag($sDraftInfoFolder, new SequenceSet($iDraftInfoUid), MessageFlag::ANSWERED);
|
||||||
MessageFlag::ANSWERED);
|
|
||||||
break;
|
break;
|
||||||
case 'forward':
|
case 'forward':
|
||||||
$sForwardedFlag = $this->Config()->Get('labs', 'imap_forwarded_flag', '');
|
$this->MailClient()->MessageSetFlag($sDraftInfoFolder, new SequenceSet($iDraftInfoUid), MessageFlag::FORWARDED);
|
||||||
if (\strlen($sForwardedFlag))
|
|
||||||
{
|
|
||||||
$this->MailClient()->MessageSetFlag($sDraftInfoFolder, new SequenceSet($iDraftInfoUid),
|
|
||||||
$sForwardedFlag);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -351,22 +345,18 @@ trait Messages
|
||||||
|
|
||||||
$mResult = true;
|
$mResult = true;
|
||||||
|
|
||||||
$sReadReceiptFlag = $this->Config()->Get('labs', 'imap_read_receipt_flag', '');
|
$sFolderFullName = $this->GetActionParam('MessageFolder', '');
|
||||||
if (!empty($sReadReceiptFlag))
|
$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', '');
|
try
|
||||||
$iUid = (int) $this->GetActionParam('MessageUid', 0);
|
|
||||||
|
|
||||||
$this->Cacher($oAccount)->Set(\RainLoop\KeyPathHelper::ReadReceiptCache($oAccount->Email(), $sFolderFullName, $iUid), '1');
|
|
||||||
|
|
||||||
if (\strlen($sFolderFullName) && 0 < $iUid)
|
|
||||||
{
|
{
|
||||||
try
|
$this->MailClient()->MessageSetFlag($sFolderFullName, new SequenceSet($iUid), MessageFlag::MDNSENT, true, true);
|
||||||
{
|
|
||||||
$this->MailClient()->MessageSetFlag($sFolderFullName, new SequenceSet($iUid), $sReadReceiptFlag, true, true);
|
|
||||||
}
|
|
||||||
catch (\Throwable $oException) {}
|
|
||||||
}
|
}
|
||||||
|
catch (\Throwable $oException) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -523,11 +513,10 @@ trait Messages
|
||||||
|
|
||||||
$sFromFolder = $this->GetActionParam('FromFolder', '');
|
$sFromFolder = $this->GetActionParam('FromFolder', '');
|
||||||
$sToFolder = $this->GetActionParam('ToFolder', '');
|
$sToFolder = $this->GetActionParam('ToFolder', '');
|
||||||
$bMarkAsRead = !empty($this->GetActionParam('MarkAsRead', '0'));
|
|
||||||
|
|
||||||
$oUids = new SequenceSet(\explode(',', (string) $this->GetActionParam('Uids', '')));
|
$oUids = new SequenceSet(\explode(',', (string) $this->GetActionParam('Uids', '')));
|
||||||
|
|
||||||
if ($bMarkAsRead)
|
if (!empty($this->GetActionParam('MarkAsRead', '0')))
|
||||||
{
|
{
|
||||||
try
|
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
|
try
|
||||||
{
|
{
|
||||||
$this->MailClient()->MessageMove($sFromFolder, $sToFolder, $oUids,
|
$this->MailClient()->MessageMove($sFromFolder, $sToFolder, $oUids,
|
||||||
|
|
|
||||||
|
|
@ -194,10 +194,12 @@ trait Response
|
||||||
'FileName' => (\strlen($sSubject) ? \MailSo\Base\Utils::ClearXss($sSubject) : 'message-'.$mResult['Uid']) . '.eml'
|
'FileName' => (\strlen($sSubject) ? \MailSo\Base\Utils::ClearXss($sSubject) : 'message-'.$mResult['Uid']) . '.eml'
|
||||||
));
|
));
|
||||||
|
|
||||||
$sForwardedFlag = \strtolower($this->Config()->Get('labs', 'imap_forwarded_flag', ''));
|
// https://datatracker.ietf.org/doc/html/rfc5788#section-3.4.1
|
||||||
$sReadReceiptFlag = \strtolower($this->Config()->Get('labs', 'imap_read_receipt_flag', ''));
|
$key = \array_search('$readreceipt', $mResult['Flags']);
|
||||||
\strlen($sForwardedFlag) && \in_array($sForwardedFlag, $mResult['Flags']) && \array_push($mResult['Flags'], '$forwarded');
|
if (false !== $key) {
|
||||||
\strlen($sReadReceiptFlag) && \in_array($sReadReceiptFlag, $mResult['Flags']) && \array_push($mResult['Flags'], '$mdnsent');
|
$mResult['Flags'][$key] = '$mdnsent';
|
||||||
|
}
|
||||||
|
|
||||||
$mResult['Flags'] = \array_unique($mResult['Flags']);
|
$mResult['Flags'] = \array_unique($mResult['Flags']);
|
||||||
|
|
||||||
if ('Message' === $sParent)
|
if ('Message' === $sParent)
|
||||||
|
|
@ -220,6 +222,7 @@ trait Response
|
||||||
|
|
||||||
if (\strlen($mResult['ReadReceipt']) && !\in_array('$forwarded', $mResult['Flags']))
|
if (\strlen($mResult['ReadReceipt']) && !\in_array('$forwarded', $mResult['Flags']))
|
||||||
{
|
{
|
||||||
|
// \in_array('$mdnsent', $mResult['Flags'])
|
||||||
if (\strlen($mResult['ReadReceipt']))
|
if (\strlen($mResult['ReadReceipt']))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ abstract class AbstractConfig implements \JsonSerializable
|
||||||
}
|
}
|
||||||
|
|
||||||
$aData = \parse_ini_file($this->sFile, true);
|
$aData = \parse_ini_file($this->sFile, true);
|
||||||
if ($aSubData && \count($aSubData))
|
if ($aData && \count($aData))
|
||||||
{
|
{
|
||||||
foreach ($aData as $sSectionKey => $aSectionValue)
|
foreach ($aData as $sSectionKey => $aSectionValue)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -355,8 +355,6 @@ Enables caching in the system'),
|
||||||
'use_imap_thread' => array(true),
|
'use_imap_thread' => array(true),
|
||||||
'use_imap_move' => array(false),
|
'use_imap_move' => array(false),
|
||||||
'use_imap_expunge_all_on_delete' => 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_body_text_limit' => array(555000),
|
||||||
'imap_message_list_fast_simple_search' => array(true),
|
'imap_message_list_fast_simple_search' => array(true),
|
||||||
'imap_message_list_count_limit_trigger' => array(0),
|
'imap_message_list_count_limit_trigger' => array(0),
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,7 @@
|
||||||
data-i18n="[placeholder]GLOBAL/PASSWORD">
|
data-i18n="[placeholder]GLOBAL/PASSWORD">
|
||||||
</div>
|
</div>
|
||||||
<div id="plugin-Login-BottomControlGroup"></div>
|
<div id="plugin-Login-BottomControlGroup"></div>
|
||||||
<div class="controls">
|
<div class="controls" style="display: flex">
|
||||||
<button class="btn btn-large btn-block buttonLogin"
|
|
||||||
data-bind="command: submitCommand"
|
|
||||||
data-i18n="LOGIN/BUTTON_SIGN_IN"></button>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex">
|
|
||||||
<div class="signMeLabel" data-bind="visible: signMeVisibility, component: {
|
<div class="signMeLabel" data-bind="visible: signMeVisibility, component: {
|
||||||
name: 'CheckboxSimple',
|
name: 'CheckboxSimple',
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -45,4 +40,9 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-large btn-block buttonLogin"
|
||||||
|
data-bind="command: submitCommand"
|
||||||
|
data-i18n="LOGIN/BUTTON_SIGN_IN"></button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -133,11 +133,7 @@
|
||||||
|
|
||||||
<div class="flagParent fontastic"></div>
|
<div class="flagParent fontastic"></div>
|
||||||
|
|
||||||
<div class="senderParent actionHandle" data-bind="attr: {'title': senderClearEmailsString}">
|
<div class="senderParent actionHandle" data-bind="attr: {'title': senderClearEmailsString}, text: senderEmailsString"></div>
|
||||||
<!-- ko if: isAnswered --><i class="replyFlag fontastic">←</i><!-- /ko -->
|
|
||||||
<!-- ko if: isForwarded --><i class="forwardFlag fontastic">→</i><!-- /ko -->
|
|
||||||
<!-- ko text: senderEmailsString --><!-- /ko -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="attachmentParent actionHandle">
|
<div class="attachmentParent actionHandle">
|
||||||
<i data-bind="css: attachmentIconClass"></i>
|
<i data-bind="css: attachmentIconClass"></i>
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,7 @@
|
||||||
<div class="b-message-view-backdrop" data-bind="visible: moveAction">
|
<div class="b-message-view-backdrop" data-bind="visible: moveAction">
|
||||||
<div class="backdrop-message" data-icon="📁" data-i18n="MESSAGE/MESSAGE_VIEW_MOVE_DESC"></div>
|
<div class="backdrop-message" data-icon="📁" data-i18n="MESSAGE/MESSAGE_VIEW_MOVE_DESC"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="b-message-view-checked-helper" data-bind="visible: !message() && '' === messageError() && hasCheckedMessages()">
|
<div class="b-message-view-checked-helper" data-bind="visible: !message() && '' === messageError() && hasCheckedMessages(), text: printableCheckedMessageCount()"></div>
|
||||||
<span data-bind="text: printableCheckedMessageCount()"></span>
|
|
||||||
<i class="fontastic">✉</i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="b-message-view-desc error" data-bind="visible: !message() && '' !== messageError() && !hasCheckedMessages(), text: messageError()">
|
<div class="b-message-view-desc error" data-bind="visible: !message() && '' !== messageError() && !hasCheckedMessages(), text: messageError()">
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -63,10 +60,10 @@
|
||||||
<a href="#" tabindex="-1" data-bind="command: forwardCommand" data-icon="→" data-i18n="MESSAGE/BUTTON_FORWARD"></a>
|
<a href="#" tabindex="-1" data-bind="command: forwardCommand" data-icon="→" data-i18n="MESSAGE/BUTTON_FORWARD"></a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation">
|
<li role="presentation">
|
||||||
<a href="#" tabindex="-1" data-bind="command: editAsNewCommand" data-icon="🖉" data-i18n="MESSAGE/BUTTON_EDIT_AS_NEW"></a>
|
<a href="#" tabindex="-1" data-bind="command: forwardAsAttachmentCommand" data-icon="→" data-i18n="MESSAGE/BUTTON_FORWARD_AS_ATTACHMENT"></a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation">
|
<li role="presentation">
|
||||||
<a href="#" tabindex="-1" data-bind="command: forwardAsAttachmentCommand" data-icon="→" data-i18n="MESSAGE/BUTTON_FORWARD_AS_ATTACHMENT"></a>
|
<a href="#" tabindex="-1" data-bind="command: editAsNewCommand" data-icon="🖉" data-i18n="MESSAGE/BUTTON_EDIT_AS_NEW"></a>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
<div data-bind="attr: { css: isDraftFolder() ? 'dividerbar' : ''}">
|
<div data-bind="attr: { css: isDraftFolder() ? 'dividerbar' : ''}">
|
||||||
|
|
@ -216,8 +213,7 @@
|
||||||
data-icon="🖼" data-i18n="MESSAGE/BUTTON_SHOW_IMAGES"></div>
|
data-icon="🖼" data-i18n="MESSAGE/BUTTON_SHOW_IMAGES"></div>
|
||||||
<div class="readReceipt" data-bind="visible: !isDraftOrSentFolder() && '' !== message().readReceipt() && !message().isReadReceipt(), click: readReceipt"
|
<div class="readReceipt" data-bind="visible: !isDraftOrSentFolder() && '' !== message().readReceipt() && !message().isReadReceipt(), click: readReceipt"
|
||||||
data-icon="✉" data-i18n="MESSAGE/BUTTON_NOTIFY_READ_RECEIPT"></div>
|
data-icon="✉" data-i18n="MESSAGE/BUTTON_NOTIFY_READ_RECEIPT"></div>
|
||||||
<div class="attachmentsPlace" data-bind="visible: message().hasAttachments(),
|
<div class="attachmentsPlace" data-bind="visible: message().hasAttachments(), css: {'selection-mode' : showAttachmentControls}">
|
||||||
css: {'selection-mode' : showAttachmentControls, 'unselectedAttachmentsError': highlightUnselectedAttachments}">
|
|
||||||
<ul class="attachmentList" data-bind="foreach: message() ? message().attachments() : []">
|
<ul class="attachmentList" data-bind="foreach: message() ? message().attachments() : []">
|
||||||
<li class="attachmentItem" draggable="true"
|
<li class="attachmentItem" draggable="true"
|
||||||
data-bind="visible: !isLinked(), event: { 'dragstart': eventDragStart }, attr: { 'title': fileName }, css: {'checked': checked}">
|
data-bind="visible: !isLinked(), event: { 'dragstart': eventDragStart }, attr: { 'title': fileName }, css: {'checked': checked}">
|
||||||
|
|
@ -244,7 +240,7 @@
|
||||||
click: function () { checked(!checked()); return false }"></div>
|
click: function () { checked(!checked()); return false }"></div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<i class="fontastic controls-handle" data-bind="visible: allowAttachmentControls() && !showAttachmentControls(), click: toggleAttachmentControls">⚙</i>
|
<i class="fontastic controls-handle" data-bind="visible: allowAttachmentControls(), click: toggleAttachmentControls">⚙</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="attachmentsControls"
|
<div class="attachmentsControls"
|
||||||
|
|
@ -257,8 +253,6 @@
|
||||||
<span class="g-ui-link" data-bind="click: downloadAsZip"
|
<span class="g-ui-link" data-bind="click: downloadAsZip"
|
||||||
data-i18n="MESSAGE/LINK_DOWNLOAD_AS_ZIP"></span>
|
data-i18n="MESSAGE/LINK_DOWNLOAD_AS_ZIP"></span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<a href="#" class="close" style="margin-right: 5px;" data-bind="click: toggleAttachmentControls">×</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,10 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td data-i18n="GLOBAL/FROM"></td>
|
<td data-i18n="GLOBAL/FROM"></td>
|
||||||
<td>
|
<td>
|
||||||
|
<!-- ko if: allowIdentities -->
|
||||||
<input type="text" data-bind="textInput: from" style="width:calc(100% - 20px)">
|
<input type="text" data-bind="textInput: from" style="width:calc(100% - 20px)">
|
||||||
|
<!-- /ko -->
|
||||||
|
<span class="e-identity" data-bind="hidden: allowIdentities, text: from"></span>
|
||||||
<!-- ko if: 1 < identitiesOptions().length -->
|
<!-- ko if: 1 < identitiesOptions().length -->
|
||||||
<div class="dropdown" style="display:inline-block" data-bind="registerBootstrapDropdown: true, openDropdownTrigger: identitiesDropdownTrigger">
|
<div class="dropdown" style="display:inline-block" data-bind="registerBootstrapDropdown: true, openDropdownTrigger: identitiesDropdownTrigger">
|
||||||
<a class="dropdown-toggle" href="#" tabindex="-1" id="identity-toggle" role="button"></a>
|
<a class="dropdown-toggle" href="#" tabindex="-1" id="identity-toggle" role="button"></a>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/* SnappyMail Webmail (c) SnappyMail Team | Licensed under AGPL 3 */
|
/* SnappyMail Webmail (c) SnappyMail Team | Licensed under AGPL 3 */
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { argv } = require('yargs');
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
head: {
|
head: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue