mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-30 20:49:20 +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_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"
|
||||
|
|
|
|||
24
README.md
24
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 |
|
||||
|
|
|
|||
|
|
@ -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(',');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 && \
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.16.1
|
||||
2.16.3
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<name>SnappyMail</name>
|
||||
<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>
|
||||
<version>2.16.1</version>
|
||||
<version>2.16.3</version>
|
||||
<licence>agpl</licence>
|
||||
<author>SnappyMail Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
|
||||
<namespace>SnappyMail</namespace>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
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
|
||||
{
|
||||
$this->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';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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->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
|
||||
|
|
|
|||
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 class="form-horizontal">
|
||||
<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>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<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>
|
||||
<div class="controls" style="padding-top: 5px">
|
||||
<b data-bind="text: php"></b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
<div>
|
||||
<div class="form-horizontal">
|
||||
<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>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<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>
|
||||
<div class="controls">
|
||||
<input type="text" data-bind="value: userSkype, enable: !savingOrLoading()" />
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
</div>
|
||||
<div class="control-group">
|
||||
<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>
|
||||
<div class="controls">
|
||||
<input type="text" data-bind="value: userFacebook, enable: !savingOrLoading()" />
|
||||
|
|
@ -23,10 +23,10 @@
|
|||
</div>
|
||||
<div class="control-group">
|
||||
<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>
|
||||
|
||||
<span class="i18n" data-i18n="SETTINGS_CUSTOM_PLUGIN/BUTTON_SAVE"></span>
|
||||
<span class="i18n" data-i18n="SETTINGS_EXAMPLE_PLUGIN/BUTTON_SAVE"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
*/
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@ class Folder implements \JsonSerializable
|
|||
'Selectable' => $this->IsSelectable(),
|
||||
'Flags' => $this->FlagsLowerCase(),
|
||||
// 'Extended' => $aExtended,
|
||||
// 'PermanentFlags' => $this->oImapFolder->PermanentFlags,
|
||||
'Metadata' => $this->oImapFolder->Metadata()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -24,12 +24,7 @@
|
|||
data-i18n="[placeholder]GLOBAL/PASSWORD">
|
||||
</div>
|
||||
<div id="plugin-Login-BottomControlGroup"></div>
|
||||
<div class="controls">
|
||||
<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="controls" style="display: flex">
|
||||
<div class="signMeLabel" data-bind="visible: signMeVisibility, component: {
|
||||
name: 'CheckboxSimple',
|
||||
params: {
|
||||
|
|
@ -45,4 +40,9 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-large btn-block buttonLogin"
|
||||
data-bind="command: submitCommand"
|
||||
data-i18n="LOGIN/BUTTON_SIGN_IN"></button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -133,11 +133,7 @@
|
|||
|
||||
<div class="flagParent fontastic"></div>
|
||||
|
||||
<div class="senderParent actionHandle" data-bind="attr: {'title': senderClearEmailsString}">
|
||||
<!-- 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="senderParent actionHandle" data-bind="attr: {'title': senderClearEmailsString}, text: senderEmailsString"></div>
|
||||
|
||||
<div class="attachmentParent actionHandle">
|
||||
<i data-bind="css: attachmentIconClass"></i>
|
||||
|
|
|
|||
|
|
@ -25,10 +25,7 @@
|
|||
<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>
|
||||
<div class="b-message-view-checked-helper" data-bind="visible: !message() && '' === messageError() && hasCheckedMessages()">
|
||||
<span data-bind="text: printableCheckedMessageCount()"></span>
|
||||
<i class="fontastic">✉</i>
|
||||
</div>
|
||||
<div class="b-message-view-checked-helper" data-bind="visible: !message() && '' === messageError() && hasCheckedMessages(), text: printableCheckedMessageCount()"></div>
|
||||
|
||||
<div class="b-message-view-desc error" data-bind="visible: !message() && '' !== messageError() && !hasCheckedMessages(), text: messageError()">
|
||||
</div>
|
||||
|
|
@ -63,10 +60,10 @@
|
|||
<a href="#" tabindex="-1" data-bind="command: forwardCommand" data-icon="→" data-i18n="MESSAGE/BUTTON_FORWARD"></a>
|
||||
</li>
|
||||
<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 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>
|
||||
</div>
|
||||
<div data-bind="attr: { css: isDraftFolder() ? 'dividerbar' : ''}">
|
||||
|
|
@ -216,8 +213,7 @@
|
|||
data-icon="🖼" data-i18n="MESSAGE/BUTTON_SHOW_IMAGES"></div>
|
||||
<div class="readReceipt" data-bind="visible: !isDraftOrSentFolder() && '' !== message().readReceipt() && !message().isReadReceipt(), click: readReceipt"
|
||||
data-icon="✉" data-i18n="MESSAGE/BUTTON_NOTIFY_READ_RECEIPT"></div>
|
||||
<div class="attachmentsPlace" data-bind="visible: message().hasAttachments(),
|
||||
css: {'selection-mode' : showAttachmentControls, 'unselectedAttachmentsError': highlightUnselectedAttachments}">
|
||||
<div class="attachmentsPlace" data-bind="visible: message().hasAttachments(), css: {'selection-mode' : showAttachmentControls}">
|
||||
<ul class="attachmentList" data-bind="foreach: message() ? message().attachments() : []">
|
||||
<li class="attachmentItem" draggable="true"
|
||||
data-bind="visible: !isLinked(), event: { 'dragstart': eventDragStart }, attr: { 'title': fileName }, css: {'checked': checked}">
|
||||
|
|
@ -244,7 +240,7 @@
|
|||
click: function () { checked(!checked()); return false }"></div>
|
||||
</li>
|
||||
</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 class="attachmentsControls"
|
||||
|
|
@ -257,8 +253,6 @@
|
|||
<span class="g-ui-link" data-bind="click: downloadAsZip"
|
||||
data-i18n="MESSAGE/LINK_DOWNLOAD_AS_ZIP"></span>
|
||||
</span>
|
||||
|
||||
<a href="#" class="close" style="margin-right: 5px;" data-bind="click: toggleAttachmentControls">×</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,10 @@
|
|||
<tr>
|
||||
<td data-i18n="GLOBAL/FROM"></td>
|
||||
<td>
|
||||
<!-- ko if: allowIdentities -->
|
||||
<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 -->
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
/* SnappyMail Webmail (c) SnappyMail Team | Licensed under AGPL 3 */
|
||||
const path = require('path');
|
||||
const { argv } = require('yargs');
|
||||
|
||||
const config = {
|
||||
head: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue