mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-29 12:09:20 +03:00
Merge branch 'master' into addressbook
# Conflicts: # snappymail/v/0.0.0/app/templates/Views/User/PopupsContacts.html
This commit is contained in:
commit
f46e996570
138 changed files with 2587 additions and 1016 deletions
|
|
@ -241,7 +241,6 @@ http_expires = 3600
|
|||
server_uids = On
|
||||
|
||||
[labs]
|
||||
allow_prefetch = Off
|
||||
cache_system_data = On
|
||||
date_from_headers = On
|
||||
autocreate_system_folders = Off
|
||||
|
|
@ -305,5 +304,5 @@ dev_email = ""
|
|||
dev_password = ""
|
||||
|
||||
[version]
|
||||
current = "2.16.3"
|
||||
current = "2.17.0"
|
||||
saved = "Fri, 04 Mar 2022 08:55:26 +0000"
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -141,24 +141,24 @@ RainLoop 1.15 vs SnappyMail
|
|||
|js/* |RainLoop |Snappy |
|
||||
|--------------- |--------: |--------: |
|
||||
|admin.js |2.158.025 | 80.521 |
|
||||
|app.js |4.215.733 | 405.835 |
|
||||
|app.js |4.215.733 | 404.700 |
|
||||
|boot.js | 672.433 | 2.050 |
|
||||
|libs.js | 647.679 | 197.160 |
|
||||
|sieve.js | 0 | 86.362 |
|
||||
|polyfills.js | 325.908 | 0 |
|
||||
|serviceworker.js | 0 | 285 |
|
||||
|TOTAL |8.019.778 | 772.213 |
|
||||
|TOTAL |8.019.778 | 771.078 |
|
||||
|
||||
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||
|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 |
|
||||
|app.min.js | 516.000 | 191.607 |140.430 | 62.036 |110.657 | 53.258 |
|
||||
|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.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.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 user |1.189.690 | 287.969 |351.061 | 97.963 |292.627 | 85.347 |
|
||||
|TOTAL user+sieve |1.189.690 | 330.060 |351.061 |108.506 |292.627 | 94.858 |
|
||||
|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.979 | 46.959 | 16.774 | 14.439 |
|
||||
|app.min.css | 274.791 | 65.092 | 39.618 | 14.841 | 13.085 |
|
||||
|app.css | 340.334 | 82.180 | 46.959 | 16.944 | 14.591 |
|
||||
|app.min.css | 274.791 | 66.070 | 39.618 | 15.014 | 13.212 |
|
||||
|boot.css | | 1.326 | | 664 | 545 |
|
||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||
|admin.css | | 29.828 | | 6.777 | 5.875 |
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'External/User/ko';
|
||||
|
||||
import { SMAudio } from 'Common/Audio';
|
||||
import { isArray, pString, changeTheme } from 'Common/Utils';
|
||||
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility } from 'Common/UtilsUser';
|
||||
|
||||
|
|
@ -24,8 +25,6 @@ import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
|||
|
||||
import {
|
||||
MessageFlagsCache,
|
||||
setFolderHash,
|
||||
getFolderHash,
|
||||
getFolderInboxName,
|
||||
getFolderFromCacheList
|
||||
} from 'Common/Cache';
|
||||
|
|
@ -222,38 +221,33 @@ export class AppUser extends AbstractApp {
|
|||
(iError, data) => {
|
||||
if (!iError && data.Result) {
|
||||
const result = data.Result,
|
||||
hash = getFolderHash(result.Folder),
|
||||
folderFromCache = getFolderFromCacheList(result.Folder);
|
||||
if (folderFromCache) {
|
||||
const oldHash = folderFromCache.hash,
|
||||
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
|
||||
|
||||
// folderFromCache.revivePropertiesFromJson(result);
|
||||
folderFromCache.expires = Date.now();
|
||||
|
||||
setFolderHash(result.Folder, result.Hash);
|
||||
|
||||
folderFromCache.messageCountAll(result.MessageCount);
|
||||
|
||||
let unreadCountChange = (folderFromCache.messageCountUnread() !== result.MessageUnseenCount);
|
||||
|
||||
folderFromCache.messageCountUnread(result.MessageUnseenCount);
|
||||
folderFromCache.uidNext = result.UidNext;
|
||||
folderFromCache.hash = result.Hash;
|
||||
folderFromCache.totalEmails(result.totalEmails);
|
||||
folderFromCache.unreadEmails(result.unreadEmails);
|
||||
|
||||
if (unreadCountChange) {
|
||||
MessageFlagsCache.clearFolder(folderFromCache.fullName);
|
||||
}
|
||||
|
||||
if (result.Flags.length) {
|
||||
result.Flags.forEach(message =>
|
||||
if (result.MessagesFlags.length) {
|
||||
result.MessagesFlags.forEach(message =>
|
||||
MessageFlagsCache.setFor(folderFromCache.fullName, message.Uid.toString(), message.Flags)
|
||||
);
|
||||
|
||||
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
||||
}
|
||||
|
||||
MessagelistUserStore.initUidNextAndNewMessages(
|
||||
folderFromCache.fullName,
|
||||
result.UidNext,
|
||||
result.NewMessages
|
||||
);
|
||||
MessagelistUserStore.notifyNewMessages(folderFromCache.fullName, result.NewMessages);
|
||||
|
||||
if (!hash || unreadCountChange || result.Hash !== hash) {
|
||||
if (!oldHash || unreadCountChange || result.Hash !== oldHash) {
|
||||
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
|
||||
MessagelistUserStore.reload();
|
||||
} else if (getFolderInboxName() === folderFromCache.fullName) {
|
||||
|
|
@ -296,8 +290,8 @@ export class AppUser extends AbstractApp {
|
|||
if (SettingsGet('Auth')) {
|
||||
rl.setWindowTitle(i18n('GLOBAL/LOADING'));
|
||||
|
||||
NotificationUserStore.enableSoundNotification(!!SettingsGet('SoundNotification'));
|
||||
NotificationUserStore.enableDesktopNotification(!!SettingsGet('DesktopNotifications'));
|
||||
SMAudio.notifications(!!SettingsGet('SoundNotification'));
|
||||
NotificationUserStore.enabled(!!SettingsGet('DesktopNotifications'));
|
||||
|
||||
AccountUserStore.email(SettingsGet('Email'));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import * as Links from 'Common/Links';
|
||||
import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals';
|
||||
import { addObservablesTo } from 'External/ko';
|
||||
|
||||
let notificator = null,
|
||||
player = null,
|
||||
|
|
@ -45,12 +46,12 @@ let notificator = null,
|
|||
'keydown','keyup'
|
||||
],
|
||||
unlock = () => {
|
||||
unlockEvents.forEach(type => doc.removeEventListener(type, unlock, true));
|
||||
if (audioCtx) {
|
||||
console.log('AudioContext ' + audioCtx.state);
|
||||
audioCtx.resume();
|
||||
}
|
||||
unlockEvents.forEach(type => doc.removeEventListener(type, unlock, true));
|
||||
// setTimeout(()=>Audio.playNotification(1),1);
|
||||
// setTimeout(()=>SMAudio.playNotification(0,1),1);
|
||||
};
|
||||
|
||||
if (audioCtx) {
|
||||
|
|
@ -76,6 +77,10 @@ export const SMAudio = new class {
|
|||
addEventsListener(player, ['ended','error'], stopFn);
|
||||
addEventListener('audio.api.stop', stopFn);
|
||||
}
|
||||
|
||||
addObservablesTo(this, {
|
||||
notifications: false
|
||||
});
|
||||
}
|
||||
|
||||
paused() {
|
||||
|
|
@ -103,18 +108,23 @@ export const SMAudio = new class {
|
|||
this.supportedWav && play(url, name);
|
||||
}
|
||||
|
||||
playNotification(silent) {
|
||||
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
|
||||
notificator = notificator || createNewObject();
|
||||
if (notificator) {
|
||||
notificator.src = Links.staticLink('sounds/'
|
||||
+ SettingsGet('NotificationSound')
|
||||
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
|
||||
notificator.volume = silent ? 0.01 : 1;
|
||||
notificator.play();
|
||||
/**
|
||||
* Used with SoundNotification setting
|
||||
*/
|
||||
playNotification(force, silent) {
|
||||
if (force || this.notifications()) {
|
||||
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
|
||||
notificator = notificator || createNewObject();
|
||||
if (notificator) {
|
||||
notificator.src = Links.staticLink('sounds/'
|
||||
+ SettingsGet('NotificationSound')
|
||||
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
|
||||
notificator.volume = silent ? 0.01 : 1;
|
||||
notificator.play();
|
||||
}
|
||||
} else {
|
||||
console.log('No audio: ' + audioCtx.state);
|
||||
}
|
||||
} else {
|
||||
console.log('No audio: ' + audioCtx.state);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import { isArray } from 'Common/Utils';
|
|||
let FOLDERS_CACHE = {},
|
||||
FOLDERS_NAME_CACHE = {},
|
||||
MESSAGE_FLAGS_CACHE = {},
|
||||
NEW_MESSAGE_CACHE = {},
|
||||
REQUESTED_MESSAGE_CACHE = {},
|
||||
inboxFolderName = 'INBOX';
|
||||
|
||||
export const
|
||||
|
|
@ -16,8 +14,6 @@ export const
|
|||
FOLDERS_CACHE = {};
|
||||
FOLDERS_NAME_CACHE = {};
|
||||
MESSAGE_FLAGS_CACHE = {};
|
||||
NEW_MESSAGE_CACHE = {};
|
||||
REQUESTED_MESSAGE_CACHE = {};
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -27,42 +23,6 @@ export const
|
|||
*/
|
||||
getMessageKey = (folderFullName, uid) => folderFullName + '#' + uid,
|
||||
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {string} uid
|
||||
*/
|
||||
addRequestedMessage = (folder, uid) => REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true,
|
||||
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {string} uid
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasRequestedMessage = (folder, uid) => true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)],
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
*/
|
||||
addNewMessageCache = (folderFullName, uid) => NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = true,
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
*/
|
||||
hasNewMessageAndRemoveFromCache = (folderFullName, uid) => {
|
||||
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)]) {
|
||||
NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
clearNewMessageCache = () => NEW_MESSAGE_CACHE = {},
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
|
|
@ -91,13 +51,6 @@ export const
|
|||
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @returns {string}
|
||||
*/
|
||||
getFolderHash = folderFullName =>
|
||||
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].hash : '',
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} folderHash
|
||||
|
|
@ -105,20 +58,6 @@ export const
|
|||
setFolderHash = (folderFullName, folderHash) =>
|
||||
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @returns {string}
|
||||
*/
|
||||
getFolderUidNext = folderFullName =>
|
||||
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].uidNext : 0,
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uidNext
|
||||
*/
|
||||
setFolderUidNext = (folderFullName, uidNext) =>
|
||||
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].uidNext = uidNext),
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @returns {?FolderModel}
|
||||
|
|
@ -182,23 +121,19 @@ export class MessageFlagsCache
|
|||
if (message) {
|
||||
const uid = message.uid,
|
||||
flags = this.getFor(message.folder, uid),
|
||||
thread = message.threads;
|
||||
thread = message.threads();
|
||||
|
||||
if (isArray(flags)) {
|
||||
message.flags(flags);
|
||||
}
|
||||
|
||||
if (thread.length) {
|
||||
const unseenSubUid = thread.find(iSubUid =>
|
||||
message.hasUnseenSubMessage(!!thread.find(iSubUid =>
|
||||
(uid !== iSubUid) && !this.hasFlag(message.folder, iSubUid, '\\seen')
|
||||
);
|
||||
|
||||
const flaggedSubUid = thread.find(iSubUid =>
|
||||
));
|
||||
message.hasFlaggedSubMessage(!!thread.find(iSubUid =>
|
||||
(uid !== iSubUid) && this.hasFlag(message.folder, iSubUid, '\\flagged')
|
||||
);
|
||||
|
||||
message.hasUnseenSubMessage(!!unseenSubUid);
|
||||
message.hasFlaggedSubMessage(!!flaggedSubUid);
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -207,9 +142,7 @@ export class MessageFlagsCache
|
|||
* @param {(MessageModel|null)} message
|
||||
*/
|
||||
static store(message) {
|
||||
if (message) {
|
||||
this.setFor(message.folder, message.uid, message.flags());
|
||||
}
|
||||
message && this.setFor(message.folder, message.uid, message.flags());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@ import { isArray, arrayLength } from 'Common/Utils';
|
|||
import {
|
||||
MessageFlagsCache,
|
||||
setFolderHash,
|
||||
getFolderHash,
|
||||
getFolderInboxName,
|
||||
getFolderFromCacheList,
|
||||
getFolderUidNext
|
||||
getFolderFromCacheList
|
||||
} from 'Common/Cache';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
|
|
@ -34,17 +32,18 @@ sortFolders = folders => {
|
|||
*/
|
||||
fetchFolderInformation = (fCallback, folder, list = []) => {
|
||||
let fetch = !arrayLength(list);
|
||||
const uids = [];
|
||||
const uids = [],
|
||||
folderFromCache = getFolderFromCacheList(folder);
|
||||
|
||||
if (!fetch) {
|
||||
list.forEach(messageListItem => {
|
||||
if (!MessageFlagsCache.getFor(messageListItem.folder, messageListItem.uid)) {
|
||||
if (!MessageFlagsCache.getFor(folder, messageListItem.uid)) {
|
||||
uids.push(messageListItem.uid);
|
||||
}
|
||||
|
||||
if (messageListItem.threads.length) {
|
||||
messageListItem.threads.forEach(uid => {
|
||||
if (!MessageFlagsCache.getFor(messageListItem.folder, uid)) {
|
||||
if (!MessageFlagsCache.getFor(folder, uid)) {
|
||||
uids.push(uid);
|
||||
}
|
||||
});
|
||||
|
|
@ -57,7 +56,7 @@ fetchFolderInformation = (fCallback, folder, list = []) => {
|
|||
Remote.request('FolderInformation', fCallback, {
|
||||
Folder: folder,
|
||||
FlagsUids: uids,
|
||||
UidNext: getFolderUidNext(folder) // Used to check for new messages
|
||||
UidNext: (folderFromCache && folderFromCache.uidNext) || 0 // Used to check for new messages
|
||||
});
|
||||
} else if (SettingsUserStore.useThreads()) {
|
||||
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
||||
|
|
@ -140,25 +139,23 @@ folderInformationMultiply = (boot = false) => {
|
|||
if (!iError && arrayLength(oData.Result)) {
|
||||
const utc = Date.now();
|
||||
oData.Result.forEach(item => {
|
||||
const hash = getFolderHash(item.Folder),
|
||||
folder = getFolderFromCacheList(item.Folder);
|
||||
const folder = getFolderFromCacheList(item.Folder);
|
||||
|
||||
if (folder) {
|
||||
const oldHash = folder.hash,
|
||||
unreadCountChange = folder.unreadEmails() !== item.unreadEmails;
|
||||
|
||||
// folder.revivePropertiesFromJson(item);
|
||||
folder.expires = utc;
|
||||
|
||||
setFolderHash(item.Folder, item.Hash);
|
||||
|
||||
folder.messageCountAll(item.MessageCount);
|
||||
|
||||
let unreadCountChange = folder.messageCountUnread() !== item.MessageUnseenCount;
|
||||
|
||||
folder.messageCountUnread(item.MessageUnseenCount);
|
||||
folder.hash = item.Hash;
|
||||
folder.totalEmails(item.totalEmails);
|
||||
folder.unreadEmails(item.unreadEmails);
|
||||
|
||||
if (unreadCountChange) {
|
||||
MessageFlagsCache.clearFolder(folder.fullName);
|
||||
}
|
||||
|
||||
if (!hash || item.Hash !== hash) {
|
||||
if (!oldHash || item.Hash !== oldHash) {
|
||||
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
|
||||
MessagelistUserStore.reload();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ const
|
|||
stripTracking = text => text
|
||||
.replace(/tracking\.(printabout\.nl[^?]+)\?.*/gsi, (...m) => m[1])
|
||||
.replace(/^.+awstrack\.me\/.+(https:%2F%2F[^/]+)/gsi, (...m) => decodeURIComponent(m[1]))
|
||||
.replace(/([?&])utm_[a-z]+=[^&?#]*/gsi, '$1')
|
||||
.replace(/([?&])utm_[a-z]+=[^&?#]*/gsi, '$1') // Urchin Tracking Module
|
||||
.replace(/([?&])ec_[a-z]+=[^&?#]*/gsi, '$1') // Sitecore
|
||||
.replace(/&&+/, '');
|
||||
|
||||
export const
|
||||
|
|
@ -99,7 +100,7 @@ export const
|
|||
'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
|
||||
],
|
||||
nonEmptyTags = [
|
||||
'A','B','EM','I','SPAN','STRONG','O:P','TABLE'
|
||||
'A','B','EM','I','SPAN','STRONG','TABLE'
|
||||
];
|
||||
|
||||
tpl.innerHTML = html
|
||||
|
|
@ -131,13 +132,12 @@ export const
|
|||
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
|
||||
// || (oStyle.maxWidth && 1 > parseFloat(oStyle.maxWidth)
|
||||
// || ('0' === oStyle.opacity
|
||||
|| (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim() && !oElement.querySelector('img')))
|
||||
) {
|
||||
oElement.remove();
|
||||
return;
|
||||
}
|
||||
// if (['CENTER','FORM'].includes(name)) {
|
||||
if ('FORM' === name || 'O:P' === name) {
|
||||
if ('FORM' === name || 'O:P' === name || (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim()))) {
|
||||
replaceWithChildren(oElement);
|
||||
return;
|
||||
}
|
||||
|
|
@ -180,6 +180,7 @@ export const
|
|||
let value;
|
||||
|
||||
// if ('TABLE' === name || 'TD' === name || 'TH' === name) {
|
||||
if (!oStyle.backgroundImage) {
|
||||
if (hasAttribute('width')) {
|
||||
value = getAttribute('width');
|
||||
oStyle.width = value.includes('%') ? value : value + 'px';
|
||||
|
|
@ -201,6 +202,7 @@ export const
|
|||
if (value && !value.includes('%')) {
|
||||
oStyle.maxHeight = value;
|
||||
}
|
||||
}
|
||||
// } else
|
||||
if ('A' === name) {
|
||||
value = oElement.href;
|
||||
|
|
@ -301,17 +303,16 @@ export const
|
|||
oStyle.removeProperty('cursor');
|
||||
oStyle.removeProperty('min-width');
|
||||
|
||||
const urls = {
|
||||
remote: [], // 'data-x-style-url'
|
||||
broken: [] // 'data-x-broken-style-src'
|
||||
};
|
||||
const
|
||||
urls_remote = [], // 'data-x-style-url'
|
||||
urls_broken = []; // 'data-x-broken-style-src'
|
||||
['backgroundImage', 'listStyleImage', 'content'].forEach(property => {
|
||||
if (oStyle[property]) {
|
||||
let value = oStyle[property],
|
||||
found = value.match(/url\s*\(([^)]+)\)/gi);
|
||||
found = value.match(/url\s*\(([^)]+)\)/i);
|
||||
if (found) {
|
||||
oStyle[property] = null;
|
||||
found = found[0].replace(/^["'\s]+|["'\s]+$/g, '');
|
||||
found = found[1].replace(/^["'\s]+|["'\s]+$/g, '');
|
||||
let lowerUrl = found.toLowerCase();
|
||||
if ('cid:' === lowerUrl.slice(0, 4)) {
|
||||
const attachment = findAttachmentByCid(found);
|
||||
|
|
@ -320,13 +321,13 @@ export const
|
|||
attachment.isInline(true);
|
||||
attachment.isLinked(true);
|
||||
}
|
||||
} else if (/http[s]?:\/\//.test(lowerUrl) || '//' === found.slice(0, 2)) {
|
||||
} else if (/^(https?:)?\/\//.test(lowerUrl)) {
|
||||
result.hasExternals = true;
|
||||
urls.remote[property] = useProxy ? proxy(found) : found;
|
||||
urls_remote.push([property, useProxy ? proxy(found) : value]);
|
||||
} else if ('data:image/' === lowerUrl.slice(0, 11)) {
|
||||
oStyle[property] = value;
|
||||
} else {
|
||||
urls.broken[property] = found;
|
||||
urls_broken.push([property, found]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -334,11 +335,11 @@ export const
|
|||
// oStyle.removeProperty('background-image');
|
||||
// oStyle.removeProperty('list-style-image');
|
||||
|
||||
if (urls.remote.length) {
|
||||
setAttribute('data-x-style-url', JSON.stringify(urls.remote));
|
||||
if (urls_remote.length) {
|
||||
setAttribute('data-x-style-url', JSON.stringify(urls_remote));
|
||||
}
|
||||
if (urls.broken.length) {
|
||||
setAttribute('data-x-style-broken-urls', JSON.stringify(urls.broken));
|
||||
if (urls_broken.length) {
|
||||
setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken));
|
||||
}
|
||||
|
||||
if (11 > pInt(oStyle.fontSize)) {
|
||||
|
|
@ -407,9 +408,6 @@ export const
|
|||
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
|
||||
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
|
||||
|
||||
// Convert line-breaks
|
||||
forEach('br', br => br.replaceWith('\n'));
|
||||
|
||||
// lines
|
||||
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
|
||||
|
||||
|
|
@ -456,10 +454,17 @@ export const
|
|||
// Italic
|
||||
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
|
||||
|
||||
// Convert line-breaks
|
||||
tpl.innerHTML = tpl.innerHTML
|
||||
.replace(/\n{3,}/gm, '\n\n')
|
||||
.replace(/\n<br[^>]*>/g, '\n')
|
||||
.replace(/<br[^>]*>\n/g, '\n');
|
||||
forEach('br', br => br.replaceWith('\n'));
|
||||
|
||||
// Blockquotes must be last
|
||||
blockquotes(tpl.content);
|
||||
|
||||
return (tpl.content.textContent || '').replace(/\n{3,}/gm, '\n\n').trim();
|
||||
return (tpl.content.textContent || '').trim();
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { MessageFlagsCache, addRequestedMessage } from 'Common/Cache';
|
||||
import { MessageFlagsCache } from 'Common/Cache';
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
||||
import { doc, createElement, elementById, dropdowns, dropdownVisibility } from 'Common/Globals';
|
||||
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet } from 'Common/Globals';
|
||||
import { plainToHtml } from 'Common/Html';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
|
|
@ -181,20 +181,34 @@ setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
|
|||
target.removeAttribute('style');
|
||||
source.removeAttribute('style');
|
||||
}
|
||||
source.observer && source.observer.disconnect();
|
||||
// source.classList.toggle('resizable', mode);
|
||||
if (mode) {
|
||||
const length = Local.get(sClientSideKeyName+mode);
|
||||
const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode),
|
||||
setTargetPos = mode => {
|
||||
let value;
|
||||
if ('Width' == mode) {
|
||||
value = source.offsetWidth;
|
||||
target.style.left = value + 'px';
|
||||
} else {
|
||||
value = source.offsetHeight;
|
||||
target.style.top = (4 + source.offsetTop + value) + 'px';
|
||||
}
|
||||
return value;
|
||||
};
|
||||
if (length) {
|
||||
source.style[mode.toLowerCase()] = length + 'px';
|
||||
setTargetPos(mode);
|
||||
}
|
||||
if (!source.layoutResizer) {
|
||||
const resizer = createElement('div', {'class':'resizer'}),
|
||||
save = (data => Remote.saveSettings(0, data)).debounce(500),
|
||||
size = {},
|
||||
store = () => {
|
||||
if ('Width' == resizer.mode) {
|
||||
target.style.left = source.offsetWidth + 'px';
|
||||
Local.set(resizer.key+resizer.mode, source.offsetWidth);
|
||||
} else {
|
||||
target.style.top = (4 + source.offsetTop + source.offsetHeight) + 'px';
|
||||
Local.set(resizer.key+resizer.mode, source.offsetHeight);
|
||||
}
|
||||
const value = setTargetPos(resizer.mode),
|
||||
prop = resizer.key + resizer.mode;
|
||||
(value == Local.get(prop)) || Local.set(prop, value);
|
||||
(value == SettingsGet('Resizer' + prop)) || save({['Resizer' + prop]: value});
|
||||
},
|
||||
cssint = s => {
|
||||
let value = getComputedStyle(source, null)[s].replace('px', '');
|
||||
|
|
@ -235,11 +249,6 @@ setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
|
|||
source.layoutResizer.mode = mode;
|
||||
source.layoutResizer.key = sClientSideKeyName;
|
||||
source.observer && source.observer.observe(source, { box: 'border-box' });
|
||||
if (length) {
|
||||
source.style[mode] = length + 'px';
|
||||
}
|
||||
} else {
|
||||
source.observer && source.observer.disconnect();
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -290,7 +299,6 @@ populateMessageBody = (oMessage, preload) => {
|
|||
}
|
||||
*/
|
||||
isNew || message.revivePropertiesFromJson(json);
|
||||
addRequestedMessage(message.folder, message.uid);
|
||||
if (messagesDom) {
|
||||
let id = 'rl-msg-' + message.hash.replace(/[^a-zA-Z0-9]/g, ''),
|
||||
body = elementById(id);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
|||
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
||||
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
|
||||
import { getFolderFromCacheList, setFolder, setFolderInboxName, setFolderHash } from 'Common/Cache';
|
||||
import { getFolderFromCacheList, setFolder, setFolderInboxName } from 'Common/Cache';
|
||||
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
|
||||
|
||||
import * as Local from 'Storage/Client';
|
||||
|
|
@ -138,13 +138,24 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
let oCacheFolder = getFolderFromCacheList(oFolder.FullName),
|
||||
type = FolderType[getKeyByValue(SystemFolders, oFolder.FullName)];
|
||||
|
||||
if (!oCacheFolder) {
|
||||
if (oCacheFolder) {
|
||||
// oCacheFolder.revivePropertiesFromJson(oFolder);
|
||||
if (oFolder.Hash) {
|
||||
oCacheFolder.hash = oFolder.Hash;
|
||||
}
|
||||
if (null != oFolder.totalEmails) {
|
||||
oCacheFolder.totalEmails(oFolder.totalEmails);
|
||||
}
|
||||
if (null != oFolder.unreadEmails) {
|
||||
oCacheFolder.unreadEmails(oFolder.unreadEmails);
|
||||
}
|
||||
} else {
|
||||
oCacheFolder = FolderModel.reviveFromJson(oFolder);
|
||||
if (!oCacheFolder)
|
||||
return null;
|
||||
|
||||
if (1 == type) {
|
||||
oCacheFolder.type(FolderType.Inbox);
|
||||
oCacheFolder.type(type);
|
||||
setFolderInboxName(oFolder.FullName);
|
||||
}
|
||||
setFolder(oCacheFolder);
|
||||
|
|
@ -158,19 +169,6 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
|| !isArray(expandedFolders)
|
||||
|| !expandedFolders.includes(oCacheFolder.fullName));
|
||||
|
||||
if (oFolder.Extended) {
|
||||
if (oFolder.Extended.Hash) {
|
||||
setFolderHash(oCacheFolder.fullName, oFolder.Extended.Hash);
|
||||
}
|
||||
|
||||
if (null != oFolder.Extended.MessageCount) {
|
||||
oCacheFolder.messageCountAll(oFolder.Extended.MessageCount);
|
||||
}
|
||||
|
||||
if (null != oFolder.Extended.MessageUnseenCount) {
|
||||
oCacheFolder.messageCountUnread(oFolder.Extended.MessageUnseenCount);
|
||||
}
|
||||
}
|
||||
return oCacheFolder;
|
||||
});
|
||||
|
||||
|
|
@ -241,8 +239,9 @@ export class FolderModel extends AbstractModel {
|
|||
|
||||
this.exists = true;
|
||||
|
||||
// this.hash = '';
|
||||
// this.uidNext = 0;
|
||||
this.hash = '';
|
||||
// this.id = null;
|
||||
this.uidNext = null;
|
||||
|
||||
this.addObservables({
|
||||
name: '',
|
||||
|
|
@ -252,27 +251,71 @@ export class FolderModel extends AbstractModel {
|
|||
focused: false,
|
||||
selected: false,
|
||||
edited: false,
|
||||
subscribed: true,
|
||||
isSubscribed: true,
|
||||
checkable: false, // Check for new messages
|
||||
askDelete: false,
|
||||
|
||||
nameForEdit: '',
|
||||
errorMsg: '',
|
||||
|
||||
privateMessageCountAll: 0,
|
||||
privateMessageCountUnread: 0,
|
||||
totalEmailsValue: 0,
|
||||
unreadEmailsValue: 0,
|
||||
|
||||
kolabType: null,
|
||||
|
||||
collapsed: true
|
||||
collapsed: true,
|
||||
|
||||
tagsAllowed: false
|
||||
});
|
||||
|
||||
this.flags = ko.observableArray();
|
||||
this.permanentFlags = ko.observableArray();
|
||||
|
||||
this.addSubscribables({
|
||||
kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue
|
||||
kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue,
|
||||
permanentFlags: aValue => this.tagsAllowed(aValue.includes('\\*'))
|
||||
});
|
||||
|
||||
this.subFolders = ko.observableArray(new FolderCollectionModel);
|
||||
this.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 });
|
||||
|
||||
this.totalEmails = koComputable({
|
||||
read: this.totalEmailsValue,
|
||||
write: (iValue) => {
|
||||
if (isPosNumeric(iValue)) {
|
||||
this.totalEmailsValue(iValue);
|
||||
} else {
|
||||
this.totalEmailsValue.valueHasMutated();
|
||||
}
|
||||
}
|
||||
})
|
||||
.extend({ notify: 'always' });
|
||||
|
||||
this.unreadEmails = koComputable({
|
||||
read: this.unreadEmailsValue,
|
||||
write: (value) => {
|
||||
if (isPosNumeric(value)) {
|
||||
this.unreadEmailsValue(value);
|
||||
} else {
|
||||
this.unreadEmailsValue.valueHasMutated();
|
||||
}
|
||||
}
|
||||
})
|
||||
.extend({ notify: 'always' });
|
||||
/*
|
||||
https://www.rfc-editor.org/rfc/rfc8621.html#section-2
|
||||
"myRights": {
|
||||
"mayAddItems": true,
|
||||
"mayRename": false,
|
||||
"maySubmit": true,
|
||||
"mayDelete": false,
|
||||
"maySetKeywords": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"maySetSeen": true,
|
||||
"mayReadItems": true
|
||||
},
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -303,30 +346,6 @@ export class FolderModel extends AbstractModel {
|
|||
|
||||
type && 'mail' != type && folder.kolabType(type);
|
||||
|
||||
folder.messageCountAll = koComputable({
|
||||
read: folder.privateMessageCountAll,
|
||||
write: (iValue) => {
|
||||
if (isPosNumeric(iValue)) {
|
||||
folder.privateMessageCountAll(iValue);
|
||||
} else {
|
||||
folder.privateMessageCountAll.valueHasMutated();
|
||||
}
|
||||
}
|
||||
})
|
||||
.extend({ notify: 'always' });
|
||||
|
||||
folder.messageCountUnread = koComputable({
|
||||
read: folder.privateMessageCountUnread,
|
||||
write: (value) => {
|
||||
if (isPosNumeric(value)) {
|
||||
folder.privateMessageCountUnread(value);
|
||||
} else {
|
||||
folder.privateMessageCountUnread.valueHasMutated();
|
||||
}
|
||||
}
|
||||
})
|
||||
.extend({ notify: 'always' });
|
||||
|
||||
folder.addComputables({
|
||||
|
||||
isInbox: () => FolderType.Inbox === folder.type(),
|
||||
|
|
@ -336,7 +355,7 @@ export class FolderModel extends AbstractModel {
|
|||
|
||||
hasVisibleSubfolders: () => !!folder.subFolders().find(folder => folder.visible()),
|
||||
|
||||
hasSubscriptions: () => folder.subscribed() | !!folder.subFolders().find(
|
||||
hasSubscriptions: () => folder.isSubscribed() | !!folder.subFolders().find(
|
||||
oFolder => {
|
||||
const subscribed = oFolder.hasSubscriptions();
|
||||
return !oFolder.isSystemFolder() && subscribed;
|
||||
|
|
@ -359,21 +378,21 @@ export class FolderModel extends AbstractModel {
|
|||
* - hasVisibleSubfolders()
|
||||
* Or when all below conditions are true:
|
||||
* - selectable()
|
||||
* - subscribed() OR hideUnsubscribed = false
|
||||
* - isSubscribed() OR hideUnsubscribed = false
|
||||
* - FolderType.User
|
||||
* - not kolabType()
|
||||
*/
|
||||
visible: () => {
|
||||
const selectable = folder.canBeSelected(),
|
||||
visible = (folder.subscribed() | !SettingsUserStore.hideUnsubscribed()) && selectable;
|
||||
visible = (folder.isSubscribed() | !SettingsUserStore.hideUnsubscribed()) && selectable;
|
||||
return folder.hasVisibleSubfolders() | visible;
|
||||
},
|
||||
|
||||
hidden: () => !folder.selectable() && (folder.isSystemFolder() | !folder.hasVisibleSubfolders()),
|
||||
|
||||
printableUnreadCount: () => {
|
||||
const count = folder.messageCountAll(),
|
||||
unread = folder.messageCountUnread(),
|
||||
const count = folder.totalEmails(),
|
||||
unread = folder.unreadEmails(),
|
||||
type = folder.type();
|
||||
|
||||
if (count) {
|
||||
|
|
@ -413,7 +432,7 @@ export class FolderModel extends AbstractModel {
|
|||
return '';
|
||||
},
|
||||
|
||||
hasUnreadMessages: () => 0 < folder.messageCountUnread() && folder.printableUnreadCount(),
|
||||
hasUnreadMessages: () => 0 < folder.unreadEmails() && folder.printableUnreadCount(),
|
||||
|
||||
hasSubscribedUnreadMessagesSubfolders: () =>
|
||||
!!folder.subFolders().find(
|
||||
|
|
@ -428,7 +447,7 @@ export class FolderModel extends AbstractModel {
|
|||
|
||||
edited: value => value && folder.nameForEdit(folder.name()),
|
||||
|
||||
messageCountUnread: unread => {
|
||||
unreadEmails: unread => {
|
||||
if (FolderType.Inbox === folder.type()) {
|
||||
fireEvent('mailbox.inbox-unread-count', unread);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ import { AbstractModel } from 'Knoin/AbstractModel';
|
|||
|
||||
import PreviewHTML from 'Html/PreviewMessage.html';
|
||||
|
||||
//import { MessageFlagsCache } from 'Common/Cache';
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
const
|
||||
// eslint-disable-next-line max-len
|
||||
url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
|
||||
|
|
@ -32,6 +35,60 @@ const
|
|||
return result;
|
||||
},
|
||||
|
||||
ignoredTags = [
|
||||
// rfc5788
|
||||
'$forwarded',
|
||||
'$mdnsent',
|
||||
'$submitpending',
|
||||
'$submitted',
|
||||
// rfc9051
|
||||
'$junk',
|
||||
'$notjunk',
|
||||
'$phishing',
|
||||
// Mailo
|
||||
'sent',
|
||||
// KMail
|
||||
'$attachment',
|
||||
'$encrypted',
|
||||
'$error',
|
||||
'$ignored',
|
||||
'$invitation',
|
||||
'$queued',
|
||||
'$replied',
|
||||
'$sent',
|
||||
'$signed',
|
||||
'$todo',
|
||||
'$watched',
|
||||
// GMail
|
||||
'$replied',
|
||||
'$attachment',
|
||||
'$notphishing',
|
||||
'junk',
|
||||
'nonjunk',
|
||||
// Others
|
||||
'$readreceipt'
|
||||
],
|
||||
|
||||
toggleTag = (message, keyword) => {
|
||||
const lower = keyword.toLowerCase(),
|
||||
isSet = message.flags().includes(lower);
|
||||
Remote.request('MessageSetKeyword', iError => {
|
||||
if (!iError) {
|
||||
if (isSet) {
|
||||
message.flags.remove(lower);
|
||||
} else {
|
||||
message.flags.push(lower);
|
||||
}
|
||||
// MessageFlagsCache.setFor(message.folder, message.uid, message.flags());
|
||||
}
|
||||
}, {
|
||||
Folder: message.folder,
|
||||
Uids: message.uid,
|
||||
Keyword: keyword,
|
||||
SetAction: isSet ? 0 : 1
|
||||
})
|
||||
},
|
||||
|
||||
replyHelper = (emails, unic, localEmails) => {
|
||||
emails.forEach(email => {
|
||||
if (undefined === unic[email.email]) {
|
||||
|
|
@ -99,9 +156,33 @@ export class MessageModel extends AbstractModel {
|
|||
|
||||
isUnseen: () => !this.flags().includes('\\seen'),
|
||||
isFlagged: () => this.flags().includes('\\flagged'),
|
||||
isReadReceipt: () => this.flags().includes('$mdnsent')
|
||||
isReadReceipt: () => this.flags().includes('$mdnsent'),
|
||||
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
|
||||
// isPhishing: () => this.flags().includes('$phishing')
|
||||
// isPhishing: () => this.flags().includes('$phishing'),
|
||||
|
||||
tagsToHTML: () => this.flags().map(value =>
|
||||
('\\' == value[0] || ignoredTags.includes(value))
|
||||
? ''
|
||||
: '<span class="focused msgflag-'+value+'">' + i18n('MESSAGE_TAGS/'+value,0,value) + '</span>'
|
||||
).join(' '),
|
||||
|
||||
tagOptions: () => {
|
||||
const tagOptions = [];
|
||||
FolderUserStore.currentFolder().permanentFlags.forEach(value => {
|
||||
let lower = value.toLowerCase();
|
||||
if ('\\' != value[0] && !ignoredTags.includes(lower)) {
|
||||
tagOptions.push({
|
||||
css: 'msgflag-' + lower,
|
||||
value: value,
|
||||
checked: this.flags().includes(lower),
|
||||
label: i18n('MESSAGE_TAGS/'+lower, 0, lower),
|
||||
toggle: (/*obj*/) => toggleTag(this, value)
|
||||
});
|
||||
}
|
||||
});
|
||||
return tagOptions
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -299,7 +380,7 @@ 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));
|
||||
this.flags().forEach(value => classes.push('msgflag-'+value));
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
|
|
@ -545,7 +626,7 @@ export class MessageModel extends AbstractModel {
|
|||
});
|
||||
|
||||
body.querySelectorAll('[data-x-style-url]').forEach(node => {
|
||||
forEachObjectEntry(JSON.parse(node.dataset.xStyleUrl), (name, url) => node.style[name] = "url('" + url + "')");
|
||||
JSON.parse(node.dataset.xStyleUrl).forEach(data => node.style[data[0]] = "url('" + data[1] + "')");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
|||
import { MessageModel } from 'Model/Message';
|
||||
|
||||
import {
|
||||
MessageFlagsCache,
|
||||
hasNewMessageAndRemoveFromCache
|
||||
MessageFlagsCache
|
||||
} from 'Common/Cache';
|
||||
|
||||
'use strict';
|
||||
|
|
@ -16,15 +15,16 @@ export class MessageCollectionModel extends AbstractCollectionModel
|
|||
this.Filtered
|
||||
this.Folder
|
||||
this.FolderHash
|
||||
this.Limit
|
||||
this.MessageCount
|
||||
this.MessageUnseenCount
|
||||
this.FolderInfo
|
||||
this.totalEmails
|
||||
this.unreadEmails
|
||||
this.MessageResultCount
|
||||
this.UidNext
|
||||
this.ThreadUid
|
||||
this.NewMessages
|
||||
this.Offset
|
||||
this.Limit
|
||||
this.Search
|
||||
this.ThreadUid
|
||||
this.UidNext
|
||||
}
|
||||
*/
|
||||
|
||||
|
|
@ -33,16 +33,10 @@ export class MessageCollectionModel extends AbstractCollectionModel
|
|||
* @returns {MessageCollectionModel}
|
||||
*/
|
||||
static reviveFromJson(object, cached) {
|
||||
let newCount = 0;
|
||||
return super.reviveFromJson(object, message => {
|
||||
message = MessageModel.reviveFromJson(message);
|
||||
if (message) {
|
||||
if (hasNewMessageAndRemoveFromCache(message.folder, message.uid) && 5 >= newCount) {
|
||||
++newCount;
|
||||
}
|
||||
|
||||
message.deleted(false);
|
||||
|
||||
cached ? MessageFlagsCache.initMessage(message) : MessageFlagsCache.store(message);
|
||||
return message;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { pString, pInt, b64EncodeJSONSafe } from 'Common/Utils';
|
||||
|
||||
import {
|
||||
getFolderHash,
|
||||
getFolderUidNext,
|
||||
getFolderFromCacheList
|
||||
} from 'Common/Cache';
|
||||
|
||||
|
|
@ -25,13 +23,14 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
|||
messageList(fCallback, params, bSilent = false) {
|
||||
const
|
||||
sFolderFullName = pString(params.Folder),
|
||||
folderHash = getFolderHash(sFolderFullName);
|
||||
folder = getFolderFromCacheList(sFolderFullName),
|
||||
folderHash = (folder && folder.hash) || '';
|
||||
|
||||
params = Object.assign({
|
||||
Offset: 0,
|
||||
Limit: SettingsUserStore.messagesPerPage(),
|
||||
Search: '',
|
||||
UidNext: getFolderUidNext(sFolderFullName), // Used to check for new messages
|
||||
UidNext: (folder && folder.uidNext) || 0, // Used to check for new messages
|
||||
Sort: FolderUserStore.sortMode(),
|
||||
Hash: folderHash + SettingsGet('AccountHash')
|
||||
}, params);
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
|
||||
Folder: folder.fullName,
|
||||
NewFolderName: nameToEdit,
|
||||
Subscribe: folder.subscribed() ? 1 : 0
|
||||
Subscribe: folder.isSubscribed() ? 1 : 0
|
||||
})
|
||||
.then(data => {
|
||||
folder.name(nameToEdit/*data.Name*/);
|
||||
|
|
@ -125,7 +125,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
&& folderToRemove.canBeDeleted()
|
||||
&& folderToRemove.askDelete()
|
||||
) {
|
||||
if (0 < folderToRemove.privateMessageCountAll()) {
|
||||
if (0 < folderToRemove.totalEmails()) {
|
||||
// FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
|
||||
folderToRemove.errorMsg(getNotification(Notification.CantDeleteNonEmptyFolder));
|
||||
} else {
|
||||
|
|
@ -138,7 +138,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
() => {
|
||||
// folderToRemove.flags.push('\\nonexistent');
|
||||
folderToRemove.selectable(false);
|
||||
// folderToRemove.subscribed(false);
|
||||
// folderToRemove.isSubscribed(false);
|
||||
// folderToRemove.checkable(false);
|
||||
if (!folderToRemove.subFolders.length) {
|
||||
removeFolderFromCacheList(folderToRemove.fullName);
|
||||
|
|
@ -170,12 +170,12 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
}
|
||||
|
||||
toggleFolderSubscription(folder) {
|
||||
let subscribe = !folder.subscribed();
|
||||
let subscribe = !folder.isSubscribed();
|
||||
Remote.request('FolderSubscribe', null, {
|
||||
Folder: folder.fullName,
|
||||
Subscribe: subscribe ? 1 : 0
|
||||
});
|
||||
folder.subscribed(subscribe);
|
||||
folder.isSubscribed(subscribe);
|
||||
}
|
||||
|
||||
toggleFolderCheckable(folder) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { SMAudio } from 'Common/Audio';
|
||||
import { SaveSettingsStep } from 'Common/Enums';
|
||||
import { EditorDefaultType, Layout } from 'Common/EnumsUser';
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
|
|
@ -35,12 +36,12 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
this.editorDefaultType = SettingsUserStore.editorDefaultType;
|
||||
this.layout = SettingsUserStore.layout;
|
||||
|
||||
this.soundNotification = NotificationUserStore.enableSoundNotification;
|
||||
this.soundNotification = SMAudio.notifications;
|
||||
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
|
||||
this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds'));
|
||||
|
||||
this.desktopNotification = NotificationUserStore.enableDesktopNotification;
|
||||
this.isDesktopNotificationAllowed = NotificationUserStore.isDesktopNotificationAllowed;
|
||||
this.desktopNotification = NotificationUserStore.enabled;
|
||||
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
|
||||
|
||||
this.viewHTML = SettingsUserStore.viewHTML;
|
||||
this.showImages = SettingsUserStore.showImages;
|
||||
|
|
@ -133,11 +134,11 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
}
|
||||
|
||||
testSoundNotification() {
|
||||
NotificationUserStore.playSoundNotification(true);
|
||||
SMAudio.playNotification(true);
|
||||
}
|
||||
|
||||
testSystemNotification() {
|
||||
NotificationUserStore.displayDesktopNotification('SnappyMail', 'Test notification');
|
||||
NotificationUserStore.display('SnappyMail', 'Test notification');
|
||||
}
|
||||
|
||||
selectLanguage() {
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export const FolderUserStore = new class {
|
|||
folder.selectable() &&
|
||||
folder.exists &&
|
||||
timeout > folder.expires &&
|
||||
(folder.isSystemFolder() || (folder.subscribed() && (folder.checkable() || !bDisplaySpecSetting)))
|
||||
(folder.isSystemFolder() || (folder.isSubscribed() && (folder.checkable() || !bDisplaySpecSetting)))
|
||||
) {
|
||||
timeouts.push([folder.expires, folder.fullName]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { koComputable } from 'External/ko';
|
||||
|
||||
import { SMAudio } from 'Common/Audio';
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { MessageSetAction } from 'Common/EnumsUser';
|
||||
import { $htmlCL } from 'Common/Globals';
|
||||
|
|
@ -8,12 +9,9 @@ import { addObservablesTo, addComputablesTo } from 'External/ko';
|
|||
|
||||
import {
|
||||
getFolderInboxName,
|
||||
addNewMessageCache,
|
||||
setFolderUidNext,
|
||||
getFolderFromCacheList,
|
||||
setFolderHash,
|
||||
MessageFlagsCache,
|
||||
clearNewMessageCache
|
||||
MessageFlagsCache
|
||||
} from 'Common/Cache';
|
||||
|
||||
import { mailBox } from 'Common/Links';
|
||||
|
|
@ -114,45 +112,40 @@ MessagelistUserStore.hasCheckedOrSelected = koComputable(() =>
|
|||
|| MessagelistUserStore.find(item => item.checked()))
|
||||
).extend({ rateLimit: 50 });
|
||||
|
||||
MessagelistUserStore.initUidNextAndNewMessages = (folder, uidNext, newMessages) => {
|
||||
if (getFolderInboxName() === folder && uidNext) {
|
||||
if (arrayLength(newMessages)) {
|
||||
newMessages.forEach(item => addNewMessageCache(folder, item.Uid));
|
||||
MessagelistUserStore.notifyNewMessages = (folder, newMessages) => {
|
||||
if (getFolderInboxName() === folder && arrayLength(newMessages)) {
|
||||
|
||||
NotificationUserStore.playSoundNotification();
|
||||
SMAudio.playNotification();
|
||||
|
||||
const len = newMessages.length;
|
||||
if (3 < len) {
|
||||
NotificationUserStore.displayDesktopNotification(
|
||||
AccountUserStore.email(),
|
||||
i18n('MESSAGE_LIST/NEW_MESSAGE_NOTIFICATION', {
|
||||
COUNT: len
|
||||
}),
|
||||
{ Url: mailBox(newMessages[0].Folder) }
|
||||
const len = newMessages.length;
|
||||
if (3 < len) {
|
||||
NotificationUserStore.display(
|
||||
AccountUserStore.email(),
|
||||
i18n('MESSAGE_LIST/NEW_MESSAGE_NOTIFICATION', {
|
||||
COUNT: len
|
||||
}),
|
||||
{ Url: mailBox(newMessages[0].Folder) }
|
||||
);
|
||||
} else {
|
||||
newMessages.forEach(item => {
|
||||
NotificationUserStore.display(
|
||||
EmailCollectionModel.reviveFromJson(item.From).toString(),
|
||||
item.subject,
|
||||
{ Folder: item.Folder, Uid: item.Uid }
|
||||
);
|
||||
} else {
|
||||
newMessages.forEach(item => {
|
||||
NotificationUserStore.displayDesktopNotification(
|
||||
EmailCollectionModel.reviveFromJson(item.From).toString(),
|
||||
item.Subject,
|
||||
{ Folder: item.Folder, Uid: item.Uid }
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setFolderUidNext(folder, uidNext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean=} bDropPagePosition = false
|
||||
* @param {boolean=} bDropCurrenFolderCache = false
|
||||
* @param {boolean=} bDropCurrentFolderCache = false
|
||||
*/
|
||||
MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrenFolderCache = false) => {
|
||||
MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCache = false) => {
|
||||
let iOffset = (MessagelistUserStore.page() - 1) * SettingsUserStore.messagesPerPage();
|
||||
|
||||
if (bDropCurrenFolderCache) {
|
||||
if (bDropCurrentFolderCache) {
|
||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
||||
}
|
||||
|
||||
|
|
@ -186,27 +179,42 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrenFolderCache
|
|||
let unreadCountChange = false;
|
||||
|
||||
const
|
||||
folder = getFolderFromCacheList(collection.Folder);
|
||||
|
||||
folder = getFolderFromCacheList(collection.Folder),
|
||||
folderInfo = collection.FolderInfo;
|
||||
if (folder && !bCached) {
|
||||
// folder.revivePropertiesFromJson(result);
|
||||
folder.expires = Date.now();
|
||||
folder.uidNext = folderInfo.UidNext;
|
||||
folder.hash = collection.FolderHash;
|
||||
|
||||
setFolderHash(collection.Folder, collection.FolderHash);
|
||||
|
||||
if (null != collection.MessageCount) {
|
||||
folder.messageCountAll(collection.MessageCount);
|
||||
if (null != folderInfo.totalEmails) {
|
||||
folder.totalEmails(folderInfo.totalEmails);
|
||||
}
|
||||
|
||||
if (null != collection.MessageUnseenCount) {
|
||||
if (pInt(folder.messageCountUnread()) !== pInt(collection.MessageUnseenCount)) {
|
||||
if (null != folderInfo.unreadEmails) {
|
||||
if (pInt(folder.unreadEmails()) !== pInt(folderInfo.unreadEmails)) {
|
||||
unreadCountChange = true;
|
||||
MessageFlagsCache.clearFolder(folder.fullName);
|
||||
}
|
||||
|
||||
folder.messageCountUnread(collection.MessageUnseenCount);
|
||||
folder.unreadEmails(folderInfo.unreadEmails);
|
||||
}
|
||||
|
||||
MessagelistUserStore.initUidNextAndNewMessages(folder.fullName, collection.UidNext, collection.NewMessages);
|
||||
folder.flags(folderInfo.Flags);
|
||||
let flags = folderInfo.PermanentFlags;
|
||||
if (flags.includes('\\*')) {
|
||||
let i = 6;
|
||||
while (--i) {
|
||||
flags.includes('$label'+i) || flags.push('$label'+i);
|
||||
}
|
||||
}
|
||||
flags.sort((a, b) => {
|
||||
a = a.toUpperCase();
|
||||
b = b.toUpperCase();
|
||||
return (a < b) ? -1 : ((a > b) ? 1 : 0);
|
||||
});
|
||||
folder.permanentFlags(flags);
|
||||
|
||||
MessagelistUserStore.notifyNewMessages(folder.fullName, collection.NewMessages);
|
||||
}
|
||||
|
||||
MessagelistUserStore.count(collection.MessageResultCount);
|
||||
|
|
@ -231,8 +239,6 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrenFolderCache
|
|||
MessagelistUserStore(collection);
|
||||
MessagelistUserStore.isIncomplete(false);
|
||||
|
||||
clearNewMessageCache();
|
||||
|
||||
if (folder && (bCached || unreadCountChange || SettingsUserStore.useThreads())) {
|
||||
rl.app.folderInformation(folder.fullName, collection);
|
||||
}
|
||||
|
|
@ -262,27 +268,25 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrenFolderCache
|
|||
MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
|
||||
messages = messages || MessagelistUserStore.listChecked();
|
||||
|
||||
let folder = null,
|
||||
let folder,
|
||||
alreadyUnread = 0,
|
||||
rootUids = messages.map(oMessage => oMessage && oMessage.uid ? oMessage.uid : null)
|
||||
.validUnique(),
|
||||
length = rootUids.length;
|
||||
|
||||
if (sFolderFullName && length) {
|
||||
rootUids.forEach(sSubUid =>
|
||||
alreadyUnread += MessageFlagsCache.storeBySetAction(sFolderFullName, sSubUid, iSetAction)
|
||||
);
|
||||
switch (iSetAction) {
|
||||
case MessageSetAction.SetSeen:
|
||||
length = 0;
|
||||
// fallthrough is intentionally
|
||||
case MessageSetAction.UnsetSeen:
|
||||
rootUids.forEach(sSubUid =>
|
||||
alreadyUnread += MessageFlagsCache.storeBySetAction(sFolderFullName, sSubUid, iSetAction)
|
||||
);
|
||||
|
||||
folder = getFolderFromCacheList(sFolderFullName);
|
||||
if (folder) {
|
||||
folder.messageCountUnread(folder.messageCountUnread() - alreadyUnread + length);
|
||||
folder.unreadEmails(folder.unreadEmails() - alreadyUnread + length);
|
||||
}
|
||||
|
||||
Remote.request('MessageSetSeen', null, {
|
||||
Folder: sFolderFullName,
|
||||
Uids: rootUids.join(','),
|
||||
|
|
@ -292,9 +296,6 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
|
|||
|
||||
case MessageSetAction.SetFlag:
|
||||
case MessageSetAction.UnsetFlag:
|
||||
rootUids.forEach(sSubUid =>
|
||||
MessageFlagsCache.storeBySetAction(sFolderFullName, sSubUid, iSetAction)
|
||||
);
|
||||
Remote.request('MessageSetFlagged', null, {
|
||||
Folder: sFolderFullName,
|
||||
Uids: rootUids.join(','),
|
||||
|
|
@ -334,26 +335,31 @@ MessagelistUserStore.removeMessagesFromList = (
|
|||
|
||||
messages.forEach(item => item && item.isUnseen() && ++unseenCount);
|
||||
|
||||
if (fromFolder && !copy) {
|
||||
fromFolder.messageCountAll(
|
||||
0 <= fromFolder.messageCountAll() - uidForRemove.length ? fromFolder.messageCountAll() - uidForRemove.length : 0
|
||||
);
|
||||
|
||||
if (0 < unseenCount) {
|
||||
fromFolder.messageCountUnread(
|
||||
0 <= fromFolder.messageCountUnread() - unseenCount ? fromFolder.messageCountUnread() - unseenCount : 0
|
||||
if (fromFolder) {
|
||||
fromFolder.hash = '';
|
||||
if (!copy) {
|
||||
fromFolder.totalEmails(
|
||||
0 <= fromFolder.totalEmails() - uidForRemove.length ? fromFolder.totalEmails() - uidForRemove.length : 0
|
||||
);
|
||||
|
||||
if (0 < unseenCount) {
|
||||
fromFolder.unreadEmails(
|
||||
0 <= fromFolder.unreadEmails() - unseenCount ? fromFolder.unreadEmails() - unseenCount : 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toFolder) {
|
||||
toFolder.hash = '';
|
||||
|
||||
if (trashFolder === toFolder.fullName || spamFolder === toFolder.fullName) {
|
||||
unseenCount = 0;
|
||||
}
|
||||
|
||||
toFolder.messageCountAll(toFolder.messageCountAll() + uidForRemove.length);
|
||||
toFolder.totalEmails(toFolder.totalEmails() + uidForRemove.length);
|
||||
if (0 < unseenCount) {
|
||||
toFolder.messageCountUnread(toFolder.messageCountUnread() + unseenCount);
|
||||
toFolder.unreadEmails(toFolder.unreadEmails() + unseenCount);
|
||||
}
|
||||
|
||||
toFolder.actionBlink(true);
|
||||
|
|
@ -378,14 +384,6 @@ MessagelistUserStore.removeMessagesFromList = (
|
|||
}
|
||||
}
|
||||
|
||||
if (fromFolderFullName) {
|
||||
setFolderHash(fromFolderFullName, '');
|
||||
}
|
||||
|
||||
if (toFolderFullName) {
|
||||
setFolderHash(toFolderFullName, '');
|
||||
}
|
||||
|
||||
if (MessagelistUserStore.threadUid()) {
|
||||
if (
|
||||
messageList.length &&
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { SMAudio } from 'Common/Audio';
|
||||
import * as Links from 'Common/Links';
|
||||
import { addObservablesTo } from 'External/ko';
|
||||
import { fireEvent } from 'Common/Globals';
|
||||
|
|
@ -37,36 +36,24 @@ if (WorkerNotifications && ServiceWorkerRegistration && ServiceWorkerRegistratio
|
|||
export const NotificationUserStore = new class {
|
||||
constructor() {
|
||||
addObservablesTo(this, {
|
||||
enableSoundNotification: false,
|
||||
|
||||
enableDesktopNotification: false,/*.extend({ notify: 'always' })*/
|
||||
|
||||
isDesktopNotificationAllowed: !NotificationsDenied()
|
||||
enabled: false,/*.extend({ notify: 'always' })*/
|
||||
allowed: !NotificationsDenied()
|
||||
});
|
||||
|
||||
this.enableDesktopNotification.subscribe(value => {
|
||||
this.enabled.subscribe(value => {
|
||||
DesktopNotifications = !!value;
|
||||
if (value && HTML5Notification && !NotificationsGranted()) {
|
||||
HTML5Notification.requestPermission(() =>
|
||||
this.isDesktopNotificationAllowed(!NotificationsDenied())
|
||||
this.allowed(!NotificationsDenied())
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Used with SoundNotification setting
|
||||
*/
|
||||
playSoundNotification(skipSetting) {
|
||||
if (skipSetting ? true : this.enableSoundNotification()) {
|
||||
SMAudio.playNotification();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used with DesktopNotifications setting
|
||||
*/
|
||||
displayDesktopNotification(title, text, messageData, imageSrc) {
|
||||
display(title, text, messageData, imageSrc) {
|
||||
if (DesktopNotifications && NotificationsGranted()) {
|
||||
const options = {
|
||||
body: text,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
@import "User/Identity.less";
|
||||
@import "User/AdvancedSearch.less";
|
||||
@import "User/Attachments.less";
|
||||
@import "User/MessageFlags.less";
|
||||
@import "User/MessageList.less";
|
||||
@import "User/MessageView.less";
|
||||
@import "User/Contacts.less";
|
||||
|
|
|
|||
|
|
@ -142,6 +142,13 @@ html.rl-side-preview-pane {
|
|||
}
|
||||
}
|
||||
|
||||
#top-system-dropdown-id::after,
|
||||
#move-dropdown-id.dropdown-toggle::after,
|
||||
#button-add-prop-dropdown-id::after {
|
||||
content: '▼';
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.dropdown-menu * + .dividerbar {
|
||||
margin-top: 9px;
|
||||
padding-top: 9px;
|
||||
|
|
@ -166,9 +173,6 @@ html.rl-side-preview-pane {
|
|||
|
||||
.btn-group.open .btn.btn-transparent.dropdown-toggle {
|
||||
color: #BD362F;
|
||||
.caret {
|
||||
border-top-color: #BD362F;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group > .btn.single {
|
||||
|
|
|
|||
26
dev/Styles/User/MessageFlags.less
Normal file
26
dev/Styles/User/MessageFlags.less
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* RFC8457 */
|
||||
.msgflag-\$important .checkboxMessage { background-color: #F00; color: #fff; }
|
||||
.msgflag-\$important.focused { background-color: rgba(255, 0, 0, 0.30); }
|
||||
.msgflag-\$important:not(.focused) { color: #F00; }
|
||||
|
||||
/* KMail tags */
|
||||
.msgflag-\$todo .checkboxMessage { background-color: #00F; color: #fff; }
|
||||
.msgflag-\$todo.focused { background-color: rgba( 64, 64, 255, 0.30); }
|
||||
.msgflag-\$todo:not(.focused) { color: #33F; }
|
||||
|
||||
/* Thunderbird labels */
|
||||
.msgflag-\$label5 .checkboxMessage { background-color: #808; color: #fff; }
|
||||
.msgflag-\$label4 .checkboxMessage { background-color: #00F; color: #fff; }
|
||||
.msgflag-\$label3 .checkboxMessage { background-color: #080; color: #fff; }
|
||||
.msgflag-\$label2 .checkboxMessage { background-color: #FA0; color: #fff; }
|
||||
.msgflag-\$label1 .checkboxMessage { background-color: #F00; color: #fff; }
|
||||
.msgflag-\$label5.focused { background-color: rgba(255, 0, 255, 0.30); }
|
||||
.msgflag-\$label4.focused { background-color: rgba( 64, 64, 255, 0.30); }
|
||||
.msgflag-\$label3.focused { background-color: rgba( 0, 255, 0, 0.30); }
|
||||
.msgflag-\$label2.focused { background-color: rgba(255, 170, 0, 0.30); }
|
||||
.msgflag-\$label1.focused { background-color: rgba(255, 0, 0, 0.30); }
|
||||
.msgflag-\$label5:not(.focused) { color: #939; }
|
||||
.msgflag-\$label4:not(.focused) { color: #33F; }
|
||||
.msgflag-\$label3:not(.focused) { color: #090; }
|
||||
.msgflag-\$label2:not(.focused) { color: #F90; }
|
||||
.msgflag-\$label1:not(.focused) { color: #F00; }
|
||||
|
|
@ -188,7 +188,7 @@ html:not(rl-mobile) {
|
|||
flex-wrap: wrap;
|
||||
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
|
|
@ -211,16 +211,11 @@ html:not(rl-mobile) {
|
|||
border-left-color: #ccc;
|
||||
}
|
||||
|
||||
.importantMark {
|
||||
color:red;
|
||||
margin-right:5px
|
||||
}
|
||||
|
||||
&.flag-\\deleted {
|
||||
opacity: .7;
|
||||
.subjectParent {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.priorityHigh::before {
|
||||
content: '!'; /*❗*/
|
||||
color: red;
|
||||
font-weight: bolder;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
&.deleted {
|
||||
|
|
@ -276,7 +271,7 @@ html:not(rl-mobile) {
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.threads-len span {
|
||||
.threads-len {
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 11px;
|
||||
|
|
@ -287,6 +282,9 @@ html:not(rl-mobile) {
|
|||
border-color: #666;
|
||||
}
|
||||
}
|
||||
.threads-len::after {
|
||||
content: ' ›';
|
||||
}
|
||||
|
||||
.replyFlag, .forwardFlag {
|
||||
margin-right: 0.25em;
|
||||
|
|
@ -329,26 +327,41 @@ html:not(rl-mobile) {
|
|||
border-bottom-color: rgba(57, 140, 242, 0.2);
|
||||
border-left-color: #398CF2;
|
||||
z-index: 101;
|
||||
|
||||
+ .messageListItem {
|
||||
border-bottom-color: rgba(57, 140, 242, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.flagParent {
|
||||
padding: 0 10px 0 5px;
|
||||
}
|
||||
&.flag-\\flagged .flagParent::after,
|
||||
.flagParent::after {
|
||||
content: '☆'; /*⚐*/
|
||||
}
|
||||
&.msgflag-\\flagged .flagParent::after,
|
||||
&.hasFlaggedSubMessage .flagParent::after {
|
||||
color: orange;
|
||||
content: '★'; /*⚑*/
|
||||
}
|
||||
&:not(.flag-\\flagged):not(.hasFlaggedSubMessage) .flagParent::after {
|
||||
content: '☆'; /*⚐*/
|
||||
}
|
||||
&:not(.flag-\\flagged):not(.hasFlaggedSubMessage) .flagParent:not(:hover) {
|
||||
&:not(.msgflag-\\flagged):not(.hasFlaggedSubMessage) .flagParent:not(:hover) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.senderParent::before {
|
||||
font-family: snappymail;
|
||||
}
|
||||
&.msgflag-\\answered .senderParent::before {
|
||||
content: '← ';
|
||||
}
|
||||
&.msgflag-\$forwarded .senderParent::before {
|
||||
content: '→ ';
|
||||
}
|
||||
&.msgflag-\\answered.msgflag-\$forwarded .senderParent::before {
|
||||
content: '←→ ';
|
||||
}
|
||||
&.msgflag-\\deleted {
|
||||
opacity: .7;
|
||||
.subjectParent {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html.rl-ctrl-key-pressed .messageListItem {
|
||||
|
|
@ -442,35 +455,3 @@ 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; }
|
||||
|
|
|
|||
|
|
@ -90,6 +90,14 @@ html.rl-no-preview-pane {
|
|||
}
|
||||
}
|
||||
|
||||
.messageTags {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.messageTags span span {
|
||||
border: 1px solid #808080;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.messageItemHeader {
|
||||
|
||||
background-color: #f8f8f8;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
|
||||
#top-system-dropdown-id {
|
||||
overflow: hidden;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-left: 8px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.rl-left-panel-disabled #more-list-dropdown-id + .dropdown-menu {
|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
Cc: this.cc(),
|
||||
Bcc: this.bcc(),
|
||||
ReplyTo: this.replyTo(),
|
||||
Subject: this.subject(),
|
||||
subject: this.subject(),
|
||||
DraftInfo: this.aDraftInfo,
|
||||
InReplyTo: this.sInReplyTo,
|
||||
References: this.sReferences,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { i18n, getNotification } from 'Common/Translator';
|
||||
import { setFolderHash } from 'Common/Cache';
|
||||
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
|
|
@ -43,10 +42,9 @@ export class FolderClearPopupView extends AbstractViewPopup {
|
|||
|
||||
this.clearingProcess(true);
|
||||
|
||||
folderToClear.messageCountAll(0);
|
||||
folderToClear.messageCountUnread(0);
|
||||
|
||||
setFolderHash(folderToClear.fullName, '');
|
||||
folderToClear.totalEmails(0);
|
||||
folderToClear.unreadEmails(0);
|
||||
folderToClear.hash = '';
|
||||
|
||||
Remote.request('FolderClear', iError => {
|
||||
this.clearingProcess(false);
|
||||
|
|
|
|||
|
|
@ -88,11 +88,14 @@ export class LoginUserView extends AbstractViewLogin {
|
|||
}
|
||||
|
||||
submitCommand(self, event) {
|
||||
const email = this.email().trim();
|
||||
this.email(email);
|
||||
|
||||
let form = event.target.form,
|
||||
data = new FormData(form),
|
||||
valid = form.reportValidity() && fireEvent('sm-user-login', data, 1);
|
||||
|
||||
this.emailError(!this.email());
|
||||
this.emailError(!email);
|
||||
this.passwordError(!this.password());
|
||||
this.formError(!valid);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import ko from 'ko';
|
|||
import { Scope } from 'Common/Enums';
|
||||
import { moveAction, addShortcut } from 'Common/Globals';
|
||||
import { mailBox, settings } from 'Common/Links';
|
||||
import { setFolderHash } from 'Common/Cache';
|
||||
//import { setFolderHash } from 'Common/Cache';
|
||||
import { addComputablesTo } from 'External/ko';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
|
|
@ -100,11 +100,11 @@ export class MailFolderList extends AbstractViewLeft {
|
|||
if (!SettingsUserStore.usePreviewPane()) {
|
||||
MessageUserStore.message(null);
|
||||
}
|
||||
|
||||
/*
|
||||
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
|
||||
setFolderHash(folder.fullName, '');
|
||||
}
|
||||
|
||||
*/
|
||||
hasher.setHash(
|
||||
mailBox(folder.fullNameHash, 1,
|
||||
(event.target.matches('.flag-icon') && !folder.isFlagged()) ? 'flagged' : ''
|
||||
|
|
|
|||
|
|
@ -24,9 +24,7 @@ import { i18n, initOnStartOrLangChange } from 'Common/Translator';
|
|||
|
||||
import {
|
||||
getFolderFromCacheList,
|
||||
MessageFlagsCache,
|
||||
hasRequestedMessage,
|
||||
addRequestedMessage
|
||||
MessageFlagsCache
|
||||
} from 'Common/Cache';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
|
|
@ -61,7 +59,6 @@ export class MailMessageList extends AbstractViewRight {
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
this.bPrefetch = false;
|
||||
this.emptySubjectValue = '';
|
||||
|
||||
this.iGoToUpOrDownTimeout = 0;
|
||||
|
|
@ -487,9 +484,9 @@ export class MailMessageList extends AbstractViewRight {
|
|||
});
|
||||
|
||||
if (iThreadUid) {
|
||||
folder.messageCountUnread(Math.max(0, folder.messageCountUnread() - cnt));
|
||||
folder.unreadEmails(Math.max(0, folder.unreadEmails() - cnt));
|
||||
} else {
|
||||
folder.messageCountUnread(0);
|
||||
folder.unreadEmails(0);
|
||||
}
|
||||
|
||||
MessageFlagsCache.clearFolder(sFolderFullName);
|
||||
|
|
@ -817,35 +814,6 @@ export class MailMessageList extends AbstractViewRight {
|
|||
addShortcut('arrowright', 'meta', Scope.MessageView, ()=>false);
|
||||
|
||||
addShortcut('f', 'meta', Scope.MessageList, ()=>this.advancedSearchClick());
|
||||
|
||||
if (!ThemeStore.isMobile() && SettingsCapa('Prefetch')) {
|
||||
ifvisible.idle(this.prefetchNextTick.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
prefetchNextTick() {
|
||||
if (!this.bPrefetch && !ifvisible.now() && !this.viewModelDom.hidden) {
|
||||
const message = MessagelistUserStore.find(
|
||||
item => item && !hasRequestedMessage(item.folder, item.uid)
|
||||
);
|
||||
if (message) {
|
||||
this.bPrefetch = true;
|
||||
|
||||
addRequestedMessage(message.folder, message.uid);
|
||||
|
||||
Remote.message(
|
||||
iError => {
|
||||
const next = !iError;
|
||||
setTimeout(() => {
|
||||
this.bPrefetch = false;
|
||||
next && this.prefetchNextTick();
|
||||
}, 1000);
|
||||
},
|
||||
message.folder,
|
||||
message.uid
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
advancedSearchClick() {
|
||||
|
|
|
|||
|
|
@ -147,6 +147,8 @@ export class MailMessageView extends AbstractViewRight {
|
|||
)
|
||||
},
|
||||
|
||||
tagsAllowed: () => FolderUserStore.currentFolder() ? FolderUserStore.currentFolder().tagsAllowed() : false,
|
||||
|
||||
messageVisibility: () => !MessageUserStore.loading() && !!currentMessage(),
|
||||
|
||||
canBeRepliedOrForwarded: () => !this.isDraftFolder() && this.messageVisibility(),
|
||||
|
|
@ -575,7 +577,7 @@ export class MailMessageView extends AbstractViewRight {
|
|||
MessageFolder: oMessage.folder,
|
||||
MessageUid: oMessage.uid,
|
||||
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() })
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
This app packages SnappyMail <upstream>2.16.3</upstream>.
|
||||
This app packages SnappyMail <upstream>2.17.0</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.3
|
||||
VERSION=2.17.0
|
||||
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.3
|
||||
2.17.0
|
||||
|
|
@ -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.3</version>
|
||||
<version>2.17.0</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.3" );
|
||||
return ( "2.17.0" );
|
||||
}
|
||||
|
||||
sub script_snappymail_version_desc
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"title": "SnappyMail",
|
||||
"description": "Simple, modern & fast web-based email client",
|
||||
"private": true,
|
||||
"version": "2.16.3",
|
||||
"version": "2.17.0",
|
||||
"homepage": "https://snappymail.eu",
|
||||
"author": {
|
||||
"name": "DJ Maze",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use \RainLoop\Exceptions\ClientException;
|
||||
|
||||
class ChangePasswordPoppassdPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||
class ChangePasswordFroxlorPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||
{
|
||||
const
|
||||
NAME = 'Change Password Froxlor',
|
||||
|
|
|
|||
68
plugins/change-password-hestia/driver.php
Normal file
68
plugins/change-password-hestia/driver.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
class ChangePasswordHestiaDriver
|
||||
{
|
||||
const
|
||||
NAME = 'Hestia',
|
||||
DESCRIPTION = 'Change passwords in Hestia.';
|
||||
|
||||
/**
|
||||
* @var \RainLoop\Config\Plugin
|
||||
*/
|
||||
private $oConfig = null;
|
||||
|
||||
/**
|
||||
* @var \MailSo\Log\Logger
|
||||
*/
|
||||
protected $oLogger = null;
|
||||
|
||||
function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger)
|
||||
{
|
||||
$this->oConfig = $oConfig;
|
||||
$this->oLogger = $oLogger;
|
||||
}
|
||||
|
||||
public static function isSupported() : bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function configMapping() : array
|
||||
{
|
||||
return array(
|
||||
\RainLoop\Plugins\Property::NewInstance('hestia_host')->SetLabel('Hestia Host')
|
||||
->SetDefaultValue('')
|
||||
->SetDescription('Ex: localhost or domain.com'),
|
||||
\RainLoop\Plugins\Property::NewInstance('hestia_port')->SetLabel('Hestia Port')
|
||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::INT)
|
||||
->SetDefaultValue(8083),
|
||||
\RainLoop\Plugins\Property::NewInstance('hestia_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', 'hestia_allowed_emails', ''))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sHost = $this->oConfig->Get('plugin', 'hestia_login');
|
||||
$sPort = $this->oConfig->Get('plugin', 'hestia_port');
|
||||
|
||||
$HTTP = \SnappyMail\HTTP\Request::factory();
|
||||
$postvars = array(
|
||||
'email' => $oAccount->Email(),
|
||||
'password' => $sPrevPassword,
|
||||
'new' => $sNewPassword,
|
||||
);
|
||||
$cRequest = $HTTP->doRequest('POST','https://'.$sHost.':'.$sPort.'/reset/mail/',http_build_query($postvars));
|
||||
if($cRequest -> body == '==ok=='){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
plugins/change-password-hestia/index.php
Normal file
20
plugins/change-password-hestia/index.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
use \RainLoop\Exceptions\ClientException;
|
||||
|
||||
class ChangePasswordHestiaPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||
{
|
||||
const
|
||||
NAME = 'Change Password Hestia',
|
||||
AUTHOR = 'Jaap Marcus',
|
||||
VERSION = '1.0',
|
||||
RELEASE = '2022-06-02',
|
||||
REQUIRED = '2.16.3',
|
||||
CATEGORY = 'Security',
|
||||
DESCRIPTION = 'Extension to allow users to change their passwords through HestiaCP';
|
||||
|
||||
public function Supported() : string
|
||||
{
|
||||
return 'Use Change Password plugin';
|
||||
}
|
||||
}
|
||||
|
|
@ -160,8 +160,7 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
$bResult = false;
|
||||
$oConfig = $this->Config();
|
||||
foreach ($this->getSupportedDrivers() as $name => $class) {
|
||||
$sFoundedValue = '';
|
||||
if (\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $oConfig->Get('plugin', "driver_{$name}_allowed_emails"), $sFoundedValue)) {
|
||||
if (\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $oConfig->Get('plugin', "driver_{$name}_allowed_emails"))) {
|
||||
$name = $class::NAME;
|
||||
$oLogger = $oActions->Logger();
|
||||
try
|
||||
|
|
|
|||
22
plugins/ldap-login-mapping/LICENSE
Normal file
22
plugins/ldap-login-mapping/LICENSE
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 RainLoop Team
|
||||
Copyright (c) 2018 <ludovic.pouzenc@mines-albi.fr>
|
||||
Copyright (c) 2022 <zephone@protonmail.com>
|
||||
|
||||
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
plugins/ldap-login-mapping/README
Normal file
1
plugins/ldap-login-mapping/README
Normal file
|
|
@ -0,0 +1 @@
|
|||
Plugin which allows you to set up LDAP username by email address (when IMAP/SMTP requires it)
|
||||
1
plugins/ldap-login-mapping/VERSION
Normal file
1
plugins/ldap-login-mapping/VERSION
Normal file
|
|
@ -0,0 +1 @@
|
|||
2.0
|
||||
253
plugins/ldap-login-mapping/index.php
Normal file
253
plugins/ldap-login-mapping/index.php
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
<?php
|
||||
|
||||
use \MailSo\Log\Enumerations\Type;
|
||||
use \RainLoop\Enumerations\PluginPropertyType;
|
||||
use \RainLoop\Plugins\AbstractPlugin;
|
||||
use RainLoop\Plugins\Property;
|
||||
|
||||
class LDAPLoginMappingPlugin extends AbstractPlugin
|
||||
{
|
||||
const
|
||||
NAME = 'LDAP login mapping',
|
||||
VERSION = '2.0',
|
||||
AUTHOR = 'RainLoop Team, Ludovic Pouzenc<ludovic@pouzenc.fr>, ZephOne<zephone@protonmail.com>',
|
||||
RELEASE = '2022-07-13',
|
||||
REQUIRED = '2.16.3',
|
||||
CATEGORY = 'Login',
|
||||
DESCRIPTION = 'Enable custom mapping using ldap field';
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aDomains = array();
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sSearchDomain = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sHostName = '127.0.0.1';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $iHostPort = 389;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sUsersDn = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sObjectClass = 'inetOrgPerson';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sLoginField = 'uid';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sEmailField = 'mail';
|
||||
|
||||
/**
|
||||
* @var \MailSo\Log\Logger
|
||||
*/
|
||||
private $oLogger = null;
|
||||
|
||||
public function Init(): void
|
||||
{
|
||||
$this->addHook('login.credentials', 'FilterLoginСredentials');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function Supported(): string
|
||||
{
|
||||
if (!\function_exists('ldap_connect'))
|
||||
{
|
||||
return 'The LDAP PHP extension must be installed to use this plugin';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sEmail
|
||||
* @param string $sLogin
|
||||
* @param string $sPassword
|
||||
*
|
||||
* @throws \RainLoop\Exceptions\ClientException
|
||||
*/
|
||||
public function FilterLoginСredentials(&$sEmail, &$sLogin, &$sPassword)
|
||||
{
|
||||
$this->oLogger = \MailSo\Log\Logger::SingletonInstance();
|
||||
|
||||
$this->aDomains = explode(',', $this->Config()->Get('plugin', 'domains', ''));
|
||||
$this->sSearchDomain = trim($this->Config()->Get('plugin', 'search_domain', ''));
|
||||
$this->sHostName = trim($this->Config()->Get('plugin', 'hostname', ''));
|
||||
$this->iHostPort = (int) $this->Config()->Get('plugin', 'port', 389);
|
||||
$this->sUsersDn = trim($this->Config()->Get('plugin', 'users_dn', ''));
|
||||
$this->sObjectClass = trim($this->Config()->Get('plugin', 'object_class', ''));
|
||||
$this->sLoginField = trim($this->Config()->Get('plugin', 'login_field', ''));
|
||||
$this->sEmailField = trim($this->Config()->Get('plugin', 'mail_field', ''));
|
||||
|
||||
if (0 < \strlen($this->sObjectClass) && 0 < \strlen($this->sEmailField))
|
||||
{
|
||||
$sIP = $_SERVER['REMOTE_ADDR'];
|
||||
$sResult = $this->ldapSearch($sEmail);
|
||||
if ( is_array($sResult) ) {
|
||||
$sLogin = $sResult['login'];
|
||||
$sEmail = $sResult['email'];
|
||||
}
|
||||
syslog(LOG_WARNING, "plugins/ldap-login-mapping/index.php:FilterLoginСredentials() auth try: $sIP/$sEmail, resolved as $sLogin/$sEmail");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function configMapping(): array
|
||||
{
|
||||
return [
|
||||
Property::NewInstance('domains')
|
||||
->SetLabel('LDAP enabled domains')
|
||||
->SetDefaultValue('example1.com,example2.com'),
|
||||
|
||||
Property::NewInstance('search_domain')
|
||||
->SetLabel('Forced domain')
|
||||
->SetDescription('Force this domain email for LDAP search')
|
||||
->SetDefaultValue('example.com'),
|
||||
|
||||
Property::NewInstance('hostname')
|
||||
->SetLabel('LDAP hostname')
|
||||
->SetDefaultValue('127.0.0.1'),
|
||||
|
||||
Property::NewInstance('port')
|
||||
->SetLabel('LDAP port')
|
||||
->SetType(PluginPropertyType::INT)
|
||||
->SetDefaultValue(389),
|
||||
|
||||
Property::NewInstance('users_dn')
|
||||
->SetLabel('Search base DN')
|
||||
->SetDescription('LDAP users search base DN. No tokens.')
|
||||
->SetDefaultValue('ou=People,dc=domain,dc=com'),
|
||||
|
||||
Property::NewInstance('object_class')
|
||||
->SetLabel('objectClass value')
|
||||
->SetDefaultValue('inetOrgPerson'),
|
||||
|
||||
Property::NewInstance('login_field')
|
||||
->SetLabel('Login field')
|
||||
->SetDefaultValue('uid'),
|
||||
|
||||
Property::NewInstance('mail_field')
|
||||
->SetLabel('Mail field')
|
||||
->SetDefaultValue('mail'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sEmailOrLogin
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function ldapSearch($sEmail)
|
||||
{
|
||||
$bFound = FALSE;
|
||||
foreach ( $this->aDomains as $sDomain ) {
|
||||
$sRegex = '/^[a-z0-9._-]+@' . preg_quote(trim($sDomain)) . '$/i';
|
||||
$this->oLogger->Write('DEBUG regex ' . $sRegex, Type::INFO, 'LDAP');
|
||||
if ( preg_match($sRegex, $sEmail) === 1) {
|
||||
$bFound = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( !$bFound ) {
|
||||
$this->oLogger->Write(
|
||||
'preg_match: no match in "' . $sEmail . '" for /^[a-z0-9._-]+@{configured-domains}$/i',
|
||||
Type::INFO,
|
||||
'LDAP');
|
||||
return FALSE;
|
||||
}
|
||||
$sLogin = \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail);
|
||||
|
||||
$this->oLogger->Write('ldap_connect: trying...', Type::INFO, 'LDAP');
|
||||
|
||||
$oCon = @\ldap_connect($this->sHostName, $this->iHostPort);
|
||||
if (!$oCon) return FALSE;
|
||||
|
||||
$this->oLogger->Write('ldap_connect: connected', Type::INFO, 'LDAP');
|
||||
|
||||
@\ldap_set_option($oCon, LDAP_OPT_PROTOCOL_VERSION, 3);
|
||||
|
||||
if (!@\ldap_bind($oCon)) {
|
||||
$this->logLdapError($oCon, 'ldap_bind');
|
||||
return FALSE;
|
||||
}
|
||||
$sSearchDn = $this->sUsersDn;
|
||||
$aItems = array($this->sLoginField, $this->sEmailField);
|
||||
if ( 0 < \strlen($this->sSearchDomain) ) {
|
||||
$sFilter = '(&(objectclass='.$this->sObjectClass.')(|('.$this->sEmailField.'='.$sLogin.'@'.$this->sSearchDomain.')('.$this->sLoginField.'='.$sLogin.')))';
|
||||
|
||||
} else {
|
||||
$sFilter = '(&(objectclass='.$this->sObjectClass.')(|('.$this->sEmailField.'='.$sEmail.')('.$this->sLoginField.'='.$sLogin.')))';
|
||||
}
|
||||
$this->oLogger->Write('ldap_search: start: '.$sSearchDn.' / '.$sFilter, Type::INFO, 'LDAP');
|
||||
$oS = @\ldap_search($oCon, $sSearchDn, $sFilter, $aItems, 0, 30, 30);
|
||||
if (!$oS) {
|
||||
$this->logLdapError($oCon, 'ldap_search');
|
||||
return FALSE;
|
||||
}
|
||||
$aEntries = @\ldap_get_entries($oCon, $oS);
|
||||
if (!is_array($aEntries)) {
|
||||
$this->logLdapError($oCon, 'ldap_get_entries');
|
||||
return FALSE;
|
||||
}
|
||||
if (!isset($aEntries[0])) {
|
||||
$this->logLdapError($oCon, 'ldap_get_entries (no result)');
|
||||
return FALSE;
|
||||
}
|
||||
if (!isset($aEntries[0][$this->sLoginField][0])) {
|
||||
$this->logLdapError($oCon, 'ldap_get_entries (no login)');
|
||||
return FALSE;
|
||||
}
|
||||
if (!isset($aEntries[0][$this->sEmailField][0])) {
|
||||
$this->logLdapError($oCon, 'ldap_get_entries (no mail)');
|
||||
return FALSE;
|
||||
}
|
||||
$sLogin = $aEntries[0][$this->sLoginField][0];
|
||||
$sEmail = $aEntries[0][$this->sEmailField][0];
|
||||
$this->oLogger->Write('ldap_search: found "' . $this->sLoginField . ': '.$sLogin . '" and "' . $this->sEmailField . ': '.$sEmail . '"');
|
||||
|
||||
return array(
|
||||
'login' => $sLogin,
|
||||
'email' => $sEmail,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $oCon
|
||||
* @param string $sCmd
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function logLdapError($oCon, $sCmd)
|
||||
{
|
||||
if ($this->oLogger)
|
||||
{
|
||||
$sError = $oCon ? @\ldap_error($oCon) : '';
|
||||
$iErrno = $oCon ? @\ldap_errno($oCon) : 0;
|
||||
|
||||
$this->oLogger->Write($sCmd.' error: '.$sError.' ('.$iErrno.')',
|
||||
Type::WARNING, 'LDAP');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -30,8 +30,10 @@ class OverrideSmtpCredentialsPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
$sHost = \trim($this->Config()->Get('plugin', 'smtp_host', ''));
|
||||
$sWhiteList = \trim($this->Config()->Get('plugin', 'override_users', ''));
|
||||
|
||||
if (0 < strlen($sWhiteList) && 0 < \strlen($sHost) && \RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sWhiteList))
|
||||
$sFoundValue = '';
|
||||
if (0 < strlen($sWhiteList) && 0 < \strlen($sHost) && \RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sWhiteList, $sFoundValue))
|
||||
{
|
||||
\SnappyMail\LOG::debug('SMTP Override', "{$sEmail} matched {$sFoundValue}");
|
||||
$aSmtpCredentials['Host'] = $sHost;
|
||||
$aSmtpCredentials['Port'] = (int) $this->Config()->Get('plugin', 'smtp_port', 25);
|
||||
|
||||
|
|
@ -53,6 +55,10 @@ class OverrideSmtpCredentialsPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
$aSmtpCredentials['Login'] = \trim($this->Config()->Get('plugin', 'smtp_user', ''));
|
||||
$aSmtpCredentials['Password'] = (string) $this->Config()->Get('plugin', 'smtp_password', '');
|
||||
}
|
||||
else
|
||||
{
|
||||
\SnappyMail\LOG::debug('SMTP Override', "{$sEmail} no match");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ class SnowfallOnLoginScreenPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
{
|
||||
const
|
||||
NAME = 'Snowfall on login screen',
|
||||
VERSION = '2.0',
|
||||
VERSION = '2.1',
|
||||
CATEGORY = 'Fun',
|
||||
DESCRIPTION = 'Snowfall on login screen (just for fun).';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
|
||||
if (!/iphone|ipod|ipad|android/i.test(navigator.userAgent))
|
||||
{
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (window.snowFall && window.rl && !rl.settings.get('Auth'))
|
||||
{
|
||||
window.snowFall.snow(document.getElementsByTagName('body'), {
|
||||
shadow: true, round: true, minSize: 2, maxSize: 5
|
||||
});
|
||||
}
|
||||
});
|
||||
if (window.snowFall && window.rl)
|
||||
{
|
||||
let body = document.body;
|
||||
addEventListener('sm-show-screen', e => {
|
||||
if ('login' == e.detail) {
|
||||
window.snowFall.snow(body, {
|
||||
shadow: true, round: true, minSize: 2, maxSize: 5
|
||||
});
|
||||
} else if (body.snow) {
|
||||
body.snow.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
*/
|
||||
|
||||
// Paul Irish requestAnimationFrame polyfill
|
||||
(function(window) {
|
||||
(window => {
|
||||
var lastTime = 0;
|
||||
var vendors = ['webkit', 'moz'];
|
||||
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
|
||||
|
|
@ -57,11 +57,10 @@
|
|||
}
|
||||
|
||||
if (!window.requestAnimationFrame)
|
||||
window.requestAnimationFrame = function(callback, element) {
|
||||
window.requestAnimationFrame = callback => {
|
||||
var currTime = new Date().getTime();
|
||||
var timeToCall = window.Math.max(0, 16 - (currTime - lastTime));
|
||||
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
|
||||
timeToCall);
|
||||
var id = window.setTimeout(() => callback(currTime + timeToCall), timeToCall);
|
||||
lastTime = currTime + timeToCall;
|
||||
return id;
|
||||
};
|
||||
|
|
@ -70,7 +69,7 @@
|
|||
window.cancelAnimationFrame = function(id) {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}(window));
|
||||
})(window);
|
||||
|
||||
var snowFall = (function(){
|
||||
function jSnow(){
|
||||
|
|
@ -248,28 +247,14 @@ var snowFall = (function(){
|
|||
cancelAnimationFrame(snowTimeout);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
return{
|
||||
snow : function(elements, options){
|
||||
snow : (elements, options) => {
|
||||
if(typeof(options) === 'string'){
|
||||
if(elements.length > 0){
|
||||
for(var i = 0; i < elements.length; i++){
|
||||
if(elements[i].snow){
|
||||
elements[i].snow.clear();
|
||||
}
|
||||
}
|
||||
}else{
|
||||
elements.snow.clear();
|
||||
}
|
||||
elements.snow.clear();
|
||||
}else{
|
||||
if(elements.length > 0){
|
||||
for(var i = 0; i < elements.length; i++){
|
||||
new jSnow().snow(elements[i], options);
|
||||
}
|
||||
}else{
|
||||
new jSnow().snow(elements, options);
|
||||
}
|
||||
new jSnow().snow(elements, options);
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ LABEL_TWO_FACTOR_STATUS = "Status"
|
|||
LABEL_TWO_FACTOR_SECRET = "Geheime sleutel"
|
||||
LABEL_TWO_FACTOR_BACKUP_CODES = "Backup codes"
|
||||
BUTTON_CREATE = "Nieuwe geheime sleutel aanmaken"
|
||||
BUTTON_ACTIVATE = "Activate"
|
||||
BUTTON_ACTIVATE = "Activeren"
|
||||
LINK_TEST = "test"
|
||||
BUTTON_SHOW_SECRET = "Bekijk geheime sleutel"
|
||||
BUTTON_HIDE_SECRET = "Verberg geheime sleutel"
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ abstract class HtmlUtils
|
|||
$sHtml = \str_replace('<o:p></o:p>', '', $sHtml);
|
||||
$sHtml = \str_replace('<o:p>', '<span>', $sHtml);
|
||||
$sHtml = \str_replace('</o:p>', '</span>', $sHtml);
|
||||
|
||||
/*
|
||||
if (\function_exists('tidy_repair_string')) {
|
||||
# http://tidy.sourceforge.net/docs/quickref.html
|
||||
$tidyConfig = array(
|
||||
|
|
@ -44,7 +44,7 @@ abstract class HtmlUtils
|
|||
);
|
||||
$sHtml = \tidy_repair_string($sHtml, $tidyConfig, 'utf8');
|
||||
}
|
||||
|
||||
*/
|
||||
$sHtml = \preg_replace(array(
|
||||
'/<p[^>]*><\/p>/i',
|
||||
'/<!doctype[^>]*>/i',
|
||||
|
|
|
|||
|
|
@ -103,13 +103,16 @@ trait Folders
|
|||
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
|
||||
* @throws \MailSo\Net\Exceptions\Exception
|
||||
* @throws \MailSo\Imap\Exceptions\Exception
|
||||
*
|
||||
* https://datatracker.ietf.org/doc/html/rfc9051#section-6.3.11
|
||||
*/
|
||||
public function FolderStatus(string $sFolderName) : FolderInformation
|
||||
{
|
||||
$aStatusItems = array(
|
||||
FolderResponseStatus::MESSAGES,
|
||||
FolderResponseStatus::UNSEEN,
|
||||
FolderResponseStatus::UIDNEXT
|
||||
FolderResponseStatus::UIDNEXT,
|
||||
FolderResponseStatus::UIDVALIDITY
|
||||
);
|
||||
if ($this->IsSupported('CONDSTORE')) {
|
||||
$aStatusItems[] = FolderResponseStatus::HIGHESTMODSEQ;
|
||||
|
|
@ -119,8 +122,16 @@ trait Folders
|
|||
}
|
||||
if ($this->IsSupported('OBJECTID')) {
|
||||
$aStatusItems[] = FolderResponseStatus::MAILBOXID;
|
||||
/*
|
||||
} else if ($this->IsSupported('X-DOVECOT')) {
|
||||
$aStatusItems[] = 'X-GUID';
|
||||
*/
|
||||
}
|
||||
|
||||
/* // STATUS SIZE can take a significant amount of time, therefore not active
|
||||
if ($this->IsSupported('IMAP4rev2')) {
|
||||
$aStatusItems[] = FolderResponseStatus::SIZE;
|
||||
}
|
||||
*/
|
||||
$oFolderInfo = $this->oCurrentFolderInfo;
|
||||
$bReselect = false;
|
||||
$bWritable = false;
|
||||
|
|
@ -151,6 +162,7 @@ trait Folders
|
|||
|
||||
if ($bReselect) {
|
||||
$this->selectOrExamineFolder($sFolderName, $bWritable, false);
|
||||
// $this->oCurrentFolderInfo->UNSEEN = $oInfo->UNSEEN;
|
||||
}
|
||||
|
||||
return $oInfo;
|
||||
|
|
@ -272,6 +284,9 @@ trait Folders
|
|||
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
|
||||
* @throws \MailSo\Net\Exceptions\Exception
|
||||
* @throws \MailSo\Imap\Exceptions\Exception
|
||||
*
|
||||
* REQUIRED IMAP4rev2 untagged responses: FLAGS, EXISTS, LIST
|
||||
* REQUIRED IMAP4rev2 OK untagged responses: PERMANENTFLAGS, UIDNEXT, UIDVALIDITY
|
||||
*/
|
||||
protected function selectOrExamineFolder(string $sFolderName, bool $bIsWritable, bool $bReSelectSameFolders) : FolderInformation
|
||||
{
|
||||
|
|
@ -333,7 +348,7 @@ trait Folders
|
|||
$key = $oResponse->OptionalResponse[0];
|
||||
if (\count($oResponse->OptionalResponse) > 1) {
|
||||
if ('PERMANENTFLAGS' === $key && \is_array($oResponse->OptionalResponse[1])) {
|
||||
$oResult->PermanentFlags = $oResponse->OptionalResponse[1];
|
||||
$oResult->PermanentFlags = \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oResponse->OptionalResponse[1]);
|
||||
}
|
||||
} else if ('READ-ONLY' === $key) {
|
||||
// $oResult->IsWritable = false;
|
||||
|
|
@ -348,7 +363,7 @@ trait Folders
|
|||
else if (\count($oResponse->ResponseList) > 2
|
||||
&& 'FLAGS' === $oResponse->ResponseList[1]
|
||||
&& \is_array($oResponse->ResponseList[2])) {
|
||||
$oResult->Flags = $oResponse->ResponseList[2];
|
||||
$oResult->Flags = \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oResponse->ResponseList[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -409,7 +424,11 @@ trait Folders
|
|||
}
|
||||
// RFC 8474
|
||||
if ($this->IsSupported('OBJECTID')) {
|
||||
$aTypes[] = FolderStatus::MAILBOXID;
|
||||
$aL[] = FolderStatus::MAILBOXID;
|
||||
/*
|
||||
} else if ($this->IsSupported('X-DOVECOT')) {
|
||||
$aL[] = 'X-GUID';
|
||||
*/
|
||||
}
|
||||
|
||||
$aReturnParams[] = 'STATUS';
|
||||
|
|
|
|||
|
|
@ -78,6 +78,23 @@ trait Messages
|
|||
break;
|
||||
}
|
||||
}
|
||||
if ($this->IsSupported('OBJECTID')) {
|
||||
$aFetchItems[] = FetchType::EMAILID;
|
||||
$aFetchItems[] = FetchType::THREADID;
|
||||
} else if ($this->IsSupported('X-GM-EXT-1')) {
|
||||
// https://developers.google.com/gmail/imap/imap-extensions
|
||||
$aFetchItems[] = 'X-GM-MSGID';
|
||||
$aFetchItems[] = 'X-GM-THRID';
|
||||
/*
|
||||
} else if ($this->IsSupported('X-DOVECOT')) {
|
||||
$aFetchItems[] = 'X-GUID';
|
||||
*/
|
||||
}
|
||||
/*
|
||||
if ($this->IsSupported('X-GM-EXT-1') && \in_array(FetchType::FLAGS, $aFetchItems)) {
|
||||
$aFetchItems[] = 'X-GM-LABELS';
|
||||
}
|
||||
*/
|
||||
|
||||
$aParams = array($sIndexRange, $aFetchItems);
|
||||
|
||||
|
|
@ -279,6 +296,8 @@ trait Messages
|
|||
* $sStoreAction[] = (UNCHANGEDSINCE $modsequence)
|
||||
*/
|
||||
|
||||
$aInputStoreItems = \array_map('\\MailSo\\Base\\Utils::Utf8ToUtf7Modified', $aInputStoreItems);
|
||||
|
||||
return $this->SendRequestGetResponse(
|
||||
$oRange->UID ? 'UID STORE' : 'STORE',
|
||||
array((string) $oRange, $sStoreAction, $aInputStoreItems)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ trait Quota
|
|||
if (!$this->IsSupported('QUOTA')) {
|
||||
return null;
|
||||
}
|
||||
$oResponseCollection = $this->SendRequestGetResponse(($root?'GETQUOTAROOT':'GETQUOTA') . " {$this->EscapeFolderName($sFolderName)}");
|
||||
$oResponseCollection = $this->SendRequest(($root?'GETQUOTAROOT':'GETQUOTA') . " {$this->EscapeFolderName($sFolderName)}");
|
||||
$aReturn = array(0, 0);
|
||||
foreach ($this->yieldUntaggedResponses() as $oResponse) {
|
||||
if ('QUOTA' === $oResponse->StatusOrIndex
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ abstract class FetchType
|
|||
const BINARY_SIZE = 'BINARY.SIZE';
|
||||
// RFC 4551
|
||||
const MODSEQ = 'MODSEQ';
|
||||
// RFC 8474
|
||||
const EMAILID = 'EMAILID';
|
||||
const THREADID = 'THREADID';
|
||||
|
||||
public static function BuildBodyCustomHeaderRequest(array $aHeaders, bool $bPeek = true) : string
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,4 +30,6 @@ abstract class FolderResponseStatus
|
|||
const APPENDLIMIT = 'APPENDLIMIT';
|
||||
// RFC 8474
|
||||
const MAILBOXID = 'MAILBOXID';
|
||||
// RFC 9051 IMAP4rev2
|
||||
const SIZE = 'SIZE';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,13 @@ abstract class MessageFlag
|
|||
FLAGGED = '\\Flagged',
|
||||
ANSWERED = '\\Answered',
|
||||
DRAFT = '\\Draft',
|
||||
// https://datatracker.ietf.org/doc/html/rfc9051#section-2.3.2
|
||||
FORWARDED = '$Forwarded',
|
||||
// https://datatracker.ietf.org/doc/html/rfc3503
|
||||
MDNSENT = '$MDNSent',
|
||||
// https://datatracker.ietf.org/doc/html/rfc8457
|
||||
IMPORTANT = '$Important',
|
||||
// https://datatracker.ietf.org/doc/html/rfc5788
|
||||
FORWARDED = '$Forwarded',
|
||||
// https://datatracker.ietf.org/doc/html/rfc9051#section-2.3.2
|
||||
JUNK = '$Junk',
|
||||
NOTJUNK = '$NotJunk',
|
||||
PHISHING = '$Phishing';
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ class FolderInformation implements \JsonSerializable
|
|||
|
||||
/**
|
||||
* @var array
|
||||
* Message flags
|
||||
*/
|
||||
public $Flags = array();
|
||||
|
||||
|
|
@ -61,20 +62,27 @@ class FolderInformation implements \JsonSerializable
|
|||
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return array(
|
||||
$result = array(
|
||||
'id' => $this->MAILBOXID,
|
||||
'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,
|
||||
*/
|
||||
'UidNext' => $this->UIDNEXT,
|
||||
'UidValidity' => $this->UIDVALIDITY
|
||||
);
|
||||
if (isset($this->MESSAGES)) {
|
||||
$result['totalEmails'] = $this->MESSAGES;
|
||||
$result['unreadEmails'] = $this->UNSEEN;
|
||||
}
|
||||
if (isset($this->HIGHESTMODSEQ)) {
|
||||
$result['Highestmodseq'] = $this->HIGHESTMODSEQ;
|
||||
}
|
||||
if (isset($this->APPENDLIMIT)) {
|
||||
$result['Appendlimit'] = $this->APPENDLIMIT;
|
||||
}
|
||||
if (isset($this->SIZE)) {
|
||||
$result['Size'] = $this->SIZE;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -293,6 +293,9 @@ class ImapClient extends \MailSo\Net\NetClient
|
|||
if (!$this->aCapabilityItems) {
|
||||
$this->setCapabilities($this->SendRequestGetResponse('CAPABILITY'));
|
||||
}
|
||||
/*
|
||||
$this->aCapabilityItems[] = 'X-DOVECOT';
|
||||
*/
|
||||
return $this->aCapabilityItems;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,14 @@ trait Status
|
|||
* This response also occurs as a result of a CREATE, SELECT or EXAMINE command.
|
||||
* @var string
|
||||
*/
|
||||
$MAILBOXID;
|
||||
$MAILBOXID,
|
||||
|
||||
/**
|
||||
* RFC 9051
|
||||
* The total size of the mailbox in octets.
|
||||
* @var int
|
||||
*/
|
||||
$SIZE;
|
||||
|
||||
public function getStatusItems() : array
|
||||
{
|
||||
|
|
@ -96,6 +103,8 @@ trait Status
|
|||
{
|
||||
if ('EXISTS' === $name) {
|
||||
$name = 'MESSAGES';
|
||||
} else if ('X-GUID' === $name) {
|
||||
$name = 'MAILBOXID';
|
||||
}
|
||||
if (\property_exists(__TRAIT__, $name)) {
|
||||
if ('MAILBOXID' !== $name) {
|
||||
|
|
@ -148,7 +157,9 @@ trait Status
|
|||
}
|
||||
// SELECT or EXAMINE command
|
||||
else if (\is_numeric($oResponse->ResponseList[1]) && \is_string($oResponse->ResponseList[2])) {
|
||||
$bResult = $this->setStatus($oResponse->ResponseList[2], $oResponse->ResponseList[1]);
|
||||
if ('UNSEEN' !== $oResponse->ResponseList[2]) {
|
||||
$bResult |= $this->setStatus($oResponse->ResponseList[2], $oResponse->ResponseList[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -209,8 +209,8 @@ class Folder implements \JsonSerializable
|
|||
$aStatus = $this->oImapFolder->getStatusItems();
|
||||
if ($aStatus && isset($aStatus['MESSAGES'], $aStatus['UNSEEN'], $aStatus['UIDNEXT'])) {
|
||||
$aExtended = array(
|
||||
'MessageCount' => (int) $aStatus['MESSAGES'],
|
||||
'MessageUnseenCount' => (int) $aStatus['UNSEEN'],
|
||||
'totalEmails' => (int) $aStatus['MESSAGES'],
|
||||
'unreadEmails' => (int) $aStatus['UNSEEN'],
|
||||
'UidNext' => (int) $aStatus['UIDNEXT'],
|
||||
// 'Hash' => $this->MailClient()->GenerateFolderHash(
|
||||
// $this->FullName(), $aStatus['MESSAGES'], $aStatus['UIDNEXT'],
|
||||
|
|
@ -223,13 +223,18 @@ class Folder implements \JsonSerializable
|
|||
'Name' => $this->Name(),
|
||||
'FullName' => $this->FullName(),
|
||||
'Delimiter' => (string) $this->Delimiter(),
|
||||
'Subscribed' => $this->bSubscribed,
|
||||
'isSubscribed' => $this->bSubscribed,
|
||||
'Exists' => $this->bExists,
|
||||
'Selectable' => $this->IsSelectable(),
|
||||
'Flags' => $this->FlagsLowerCase(),
|
||||
// 'Extended' => $aExtended,
|
||||
// 'PermanentFlags' => $this->oImapFolder->PermanentFlags,
|
||||
'Metadata' => $this->oImapFolder->Metadata()
|
||||
'Metadata' => $this->oImapFolder->Metadata(),
|
||||
'UidNext' => $this->oImapFolder->UIDNEXT,
|
||||
// RFC 8621
|
||||
'totalEmails' => $this->oImapFolder->MESSAGES,
|
||||
'unreadEmails' => $this->oImapFolder->UNSEEN,
|
||||
'id' => $this->oImapFolder->MAILBOXID
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,8 +131,7 @@ class MailClient
|
|||
public function MessageSetFlag(string $sFolderName, SequenceSet $oRange, string $sMessageFlag, bool $bSetAction = true, bool $bSkipUnsupportedFlag = false) : void
|
||||
{
|
||||
if (\count($oRange)) {
|
||||
$oFolderInfo = $this->oImapClient->FolderSelect($sFolderName);
|
||||
if ($oFolderInfo->IsFlagSupported($sMessageFlag)) {
|
||||
if ($this->oImapClient->FolderSelect($sFolderName)->IsFlagSupported($sMessageFlag)) {
|
||||
$sStoreAction = $bSetAction ? StoreAction::ADD_FLAGS_SILENT : StoreAction::REMOVE_FLAGS_SILENT;
|
||||
$this->oImapClient->MessageStoreFlag($oRange, array($sMessageFlag), $sStoreAction);
|
||||
} else if (!$bSkipUnsupportedFlag) {
|
||||
|
|
@ -429,20 +428,19 @@ class MailClient
|
|||
|
||||
protected function initFolderValues(string $sFolderName) : array
|
||||
{
|
||||
$aFolderStatus = $this->oImapClient->FolderStatus($sFolderName)->getStatusItems();
|
||||
|
||||
$oFolderStatus = $this->oImapClient->FolderStatus($sFolderName);
|
||||
return [
|
||||
\max(0, $aFolderStatus[FolderResponseStatus::MESSAGES] ?: 0),
|
||||
\max(0, $oFolderStatus->MESSAGES ?: 0),
|
||||
|
||||
\max(0, $aFolderStatus[FolderResponseStatus::UNSEEN] ?: 0),
|
||||
\max(0, $oFolderStatus->UNSEEN ?: 0),
|
||||
|
||||
\max(0, $aFolderStatus[FolderResponseStatus::UIDNEXT] ?: 0),
|
||||
\max(0, $oFolderStatus->UIDNEXT ?: 0),
|
||||
|
||||
\max(0, $aFolderStatus[FolderResponseStatus::HIGHESTMODSEQ] ?: 0),
|
||||
\max(0, $oFolderStatus->HIGHESTMODSEQ ?: 0),
|
||||
|
||||
$aFolderStatus[FolderResponseStatus::APPENDLIMIT] ?: $this->oImapClient->AppendLimit(),
|
||||
$oFolderStatus->APPENDLIMIT ?: $this->oImapClient->AppendLimit(),
|
||||
|
||||
$aFolderStatus[FolderResponseStatus::MAILBOXID] ?: ''
|
||||
$oFolderStatus->MAILBOXID ?: ''
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -504,7 +502,7 @@ class MailClient
|
|||
$aNewMessages[] = array(
|
||||
'Folder' => $sFolderName,
|
||||
'Uid' => $iUid,
|
||||
'Subject' => $oHeaders->ValueByName(MimeHeader::SUBJECT, !$sContentTypeCharset),
|
||||
'subject' => $oHeaders->ValueByName(MimeHeader::SUBJECT, !$sContentTypeCharset),
|
||||
'From' => $oHeaders->GetAsEmailCollection(MimeHeader::FROM_, !$sContentTypeCharset)
|
||||
);
|
||||
}
|
||||
|
|
@ -522,11 +520,16 @@ class MailClient
|
|||
public function FolderInformation(string $sFolderName, int $iPrevUidNext = 0, SequenceSet $oRange = null) : array
|
||||
{
|
||||
list($iCount, $iUnseenCount, $iUidNext, $iHighestModSeq, $iAppendLimit, $sMailboxId) = $this->initFolderValues($sFolderName);
|
||||
|
||||
/*
|
||||
// Don't use FolderExamine, else PERMANENTFLAGS is empty in Dovecot
|
||||
$oInfo = $this->oImapClient->FolderSelect($sFolderName);
|
||||
$oInfo->UNSEEN = $iUnseenCount;
|
||||
$oInfo->HIGHESTMODSEQ = $iHighestModSeq;
|
||||
$oInfo->Hash = $this->GenerateFolderHash($oInfo->FolderName, $oInfo->MESSAGES, $oInfo->UIDNEXT, $oInfo->HIGHESTMODSEQ);
|
||||
*/
|
||||
$aFlags = array();
|
||||
if ($oRange && \count($oRange)) {
|
||||
$oInfo = $this->oImapClient->FolderExamine($sFolderName);
|
||||
// $oInfo->PermanentFlags
|
||||
|
||||
$aFetchResponse = $this->oImapClient->Fetch(array(
|
||||
FetchType::UID,
|
||||
|
|
@ -535,7 +538,7 @@ class MailClient
|
|||
|
||||
foreach ($aFetchResponse as $oFetchResponse) {
|
||||
$iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID);
|
||||
$aLowerFlags = \array_map('strtolower', $oFetchResponse->GetFetchValue(FetchType::FLAGS));
|
||||
$aLowerFlags = \array_map('mb_strtolower', \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oFetchResponse->GetFetchValue(FetchType::FLAGS)));
|
||||
$aFlags[] = array(
|
||||
'Uid' => $iUid,
|
||||
'Flags' => $aLowerFlags
|
||||
|
|
@ -545,14 +548,16 @@ class MailClient
|
|||
|
||||
return array(
|
||||
'Folder' => $sFolderName,
|
||||
'Hash' => $this->GenerateFolderHash($sFolderName, $iCount, $iUidNext, $iHighestModSeq),
|
||||
'MessageCount' => $iCount,
|
||||
'MessageUnseenCount' => $iUnseenCount,
|
||||
'totalEmails' => $iCount,
|
||||
'unreadEmails' => $iUnseenCount,
|
||||
'UidNext' => $iUidNext,
|
||||
'MessagesFlags' => $aFlags,
|
||||
'HighestModSeq' => $iHighestModSeq,
|
||||
'AppendLimit' => $iAppendLimit,
|
||||
'MailboxId' => $sMailboxId,
|
||||
// 'Flags' => $oInfo->Flags,
|
||||
// 'PermanentFlags' => $oInfo->PermanentFlags,
|
||||
'Hash' => $this->GenerateFolderHash($sFolderName, $iCount, $iUidNext, $iHighestModSeq),
|
||||
'MessagesFlags' => $aFlags,
|
||||
'NewMessages' => $this->getFolderNextMessageInformation($sFolderName, $iPrevUidNext, $iUidNext)
|
||||
);
|
||||
}
|
||||
|
|
@ -823,21 +828,21 @@ class MailClient
|
|||
|
||||
$sSearch = \trim($oParams->sSearch);
|
||||
|
||||
list($iMessageRealCount, $iMessageUnseenCount, $iUidNext, $iHighestModSeq) = $this->initFolderValues($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;
|
||||
|
||||
list($iMessageRealCount, $iMessageUnseenCount, $iUidNext, $iHighestModSeq) = $this->initFolderValues($oParams->sFolderName);
|
||||
// Don't use FolderExamine, else PERMANENTFLAGS is empty in Dovecot
|
||||
$oInfo = $this->oImapClient->FolderSelect($oParams->sFolderName);
|
||||
$oInfo->UNSEEN = $iMessageUnseenCount;
|
||||
$oInfo->HIGHESTMODSEQ = $iHighestModSeq;
|
||||
// $oInfo->Hash = $this->GenerateFolderHash($oInfo->FolderName, $oInfo->MESSAGES, $oInfo->UIDNEXT, $oInfo->HIGHESTMODSEQ);
|
||||
$oMessageCollection->FolderInfo = $oInfo;
|
||||
|
||||
$aUids = array();
|
||||
$aAllThreads = [];
|
||||
|
|
@ -858,14 +863,14 @@ class MailClient
|
|||
}
|
||||
|
||||
$oMessageCollection->FolderHash = $this->GenerateFolderHash(
|
||||
$oParams->sFolderName, $iMessageRealCount, $iUidNext, $iHighestModSeq);
|
||||
|
||||
$oMessageCollection->UidNext = $iUidNext;
|
||||
$oParams->sFolderName, $iMessageRealCount, $iUidNext, $oInfo->HIGHESTMODSEQ ?: 0
|
||||
);
|
||||
|
||||
if (!$oParams->iThreadUid)
|
||||
{
|
||||
$oMessageCollection->NewMessages = $this->getFolderNextMessageInformation(
|
||||
$oParams->sFolderName, $oParams->iPrevUidNext, $iUidNext);
|
||||
$oParams->sFolderName, $oParams->iPrevUidNext, $iUidNext
|
||||
);
|
||||
}
|
||||
|
||||
$bSearch = false;
|
||||
|
|
@ -1256,17 +1261,13 @@ class MailClient
|
|||
*/
|
||||
public function FolderClear(string $sFolderFullName) : self
|
||||
{
|
||||
$oFolderInformation = $this->oImapClient->FolderSelect($sFolderFullName);
|
||||
if (0 < $oFolderInformation->MESSAGES)
|
||||
{
|
||||
if (0 < $this->oImapClient->FolderSelect($sFolderFullName)->MESSAGES) {
|
||||
$this->oImapClient->MessageStoreFlag(new SequenceSet('1:*', false),
|
||||
array(MessageFlag::DELETED),
|
||||
StoreAction::ADD_FLAGS_SILENT
|
||||
);
|
||||
|
||||
$this->oImapClient->FolderExpunge();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
@ -1275,13 +1276,10 @@ class MailClient
|
|||
*/
|
||||
public function FolderSubscribe(string $sFolderFullName, bool $bSubscribe) : self
|
||||
{
|
||||
if (!\strlen($sFolderFullName))
|
||||
{
|
||||
if (!\strlen($sFolderFullName)) {
|
||||
throw new \MailSo\Base\Exceptions\InvalidArgumentException;
|
||||
}
|
||||
|
||||
$this->oImapClient->{$bSubscribe ? 'FolderSubscribe' : 'FolderUnsubscribe'}($sFolderFullName);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ class Message implements \JsonSerializable
|
|||
$sHeaderDate = '',
|
||||
$aFlagsLowerCase = [],
|
||||
|
||||
/**
|
||||
* https://www.rfc-editor.org/rfc/rfc8474#section-5
|
||||
*/
|
||||
$sEmailId = '',
|
||||
$sThreadId = '',
|
||||
|
||||
/**
|
||||
* @var \MailSo\Mime\EmailCollection
|
||||
*/
|
||||
|
|
@ -287,16 +293,21 @@ class Message implements \JsonSerializable
|
|||
$oBodyStructure = $oFetchResponse->GetFetchBodyStructure();
|
||||
}
|
||||
|
||||
$sInternalDate = $oFetchResponse->GetFetchValue(FetchType::INTERNALDATE);
|
||||
$aFlags = $oFetchResponse->GetFetchValue(FetchType::FLAGS);
|
||||
|
||||
$oMessage->sFolder = $sFolder;
|
||||
$oMessage->iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID);
|
||||
$oMessage->iSize = (int) $oFetchResponse->GetFetchValue(FetchType::RFC822_SIZE);
|
||||
$oMessage->aFlagsLowerCase = \array_map('strtolower', $aFlags ?: []);
|
||||
$oMessage->aFlagsLowerCase = \array_map('mb_strtolower', \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $aFlags ?: []));
|
||||
$oMessage->iInternalTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseInternalDateString(
|
||||
$oFetchResponse->GetFetchValue(FetchType::INTERNALDATE)
|
||||
);
|
||||
|
||||
$oMessage->iInternalTimeStampInUTC =
|
||||
\MailSo\Base\DateTimeHelper::ParseInternalDateString($sInternalDate);
|
||||
$oMessage->sEmailId = $oFetchResponse->GetFetchValue(FetchType::EMAILID)
|
||||
// ?: $oFetchResponse->GetFetchValue('X-GUID')
|
||||
?: $oFetchResponse->GetFetchValue('X-GM-MSGID');
|
||||
$oMessage->sThreadId = $oFetchResponse->GetFetchValue(FetchType::THREADID)
|
||||
?: $oFetchResponse->GetFetchValue('X-GM-THRID');
|
||||
|
||||
$sCharset = $oBodyStructure ? Utils::NormalizeCharset($oBodyStructure->SearchCharset()) : '';
|
||||
|
||||
|
|
@ -417,9 +428,9 @@ class Message implements \JsonSerializable
|
|||
$oMessage->bIsSpam = false !== \stripos($oMessage->sSubject, '*** SPAM ***');
|
||||
} else if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_BOGOSITY)) {
|
||||
$oMessage->sSpamResult = $spam;
|
||||
$oMessage->bIsSpam = !!\preg_match('/yes|spam/', $spam);
|
||||
$oMessage->bIsSpam = !\str_contains($spam, 'Ham');
|
||||
if (\preg_match('/spamicity=([\\d\\.]+)/', $spam, $spamicity)) {
|
||||
$oMessage->setSpamScore(\floatval($spamicity[1]));
|
||||
$oMessage->setSpamScore(100 * \floatval($spamicity[1]));
|
||||
}
|
||||
} else if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_SPAM_STATUS)) {
|
||||
$oMessage->sSpamResult = $spam;
|
||||
|
|
@ -628,13 +639,20 @@ class Message implements \JsonSerializable
|
|||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
/*
|
||||
// JMAP-only RFC8621 keywords (RFC5788)
|
||||
$keywords = \array_fill_keys(\str_replace(
|
||||
['\\draft', '\\seen', '\\flagged', '\\answered'],
|
||||
[ '$draft', '$seen', '$flagged', '$answered'],
|
||||
$this->aFlagsLowerCase
|
||||
), true);
|
||||
*/
|
||||
return array(
|
||||
'@Object' => 'Object/Message',
|
||||
'Folder' => $this->sFolder,
|
||||
'Uid' => $this->iUid,
|
||||
'Subject' => \trim(Utils::Utf8Clear($this->sSubject)),
|
||||
'subject' => \trim(Utils::Utf8Clear($this->sSubject)),
|
||||
'MessageId' => $this->sMessageId,
|
||||
'Size' => $this->iSize,
|
||||
'SpamScore' => $this->bIsSpam ? 100 : $this->iSpamScore,
|
||||
'SpamResult' => $this->sSpamResult,
|
||||
'IsSpam' => $this->bIsSpam,
|
||||
|
|
@ -658,7 +676,16 @@ class Message implements \JsonSerializable
|
|||
|
||||
'Attachments' => $this->oAttachments ? $this->oAttachments->SpecData() : null,
|
||||
|
||||
'Flags' => $this->aFlagsLowerCase
|
||||
'Flags' => $this->aFlagsLowerCase,
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.1
|
||||
'id' => $this->sEmailId,
|
||||
// 'blobId' => $this->sEmailIdBlob,
|
||||
'threadId' => $this->sThreadId,
|
||||
// 'mailboxIds' => ['mailboxid'=>true],
|
||||
// 'keywords' => $keywords,
|
||||
'size' => $this->iSize,
|
||||
'receivedAt' => \gmdate('Y-m-d\\TH:i:s\\Z', $this->iInternalTimeStampInUTC)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,16 +22,6 @@ class MessageCollection extends \MailSo\Base\Collection
|
|||
*/
|
||||
public $FolderHash = '';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $MessageCount = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $MessageUnseenCount = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
|
|
@ -57,11 +47,6 @@ class MessageCollection extends \MailSo\Base\Collection
|
|||
*/
|
||||
public $Search = '';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $UidNext = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
|
|
@ -95,13 +80,10 @@ class MessageCollection extends \MailSo\Base\Collection
|
|||
public function jsonSerialize()
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), array(
|
||||
'MessageCount' => $this->MessageCount,
|
||||
'MessageUnseenCount' => $this->MessageUnseenCount,
|
||||
'MessageResultCount' => $this->MessageResultCount,
|
||||
'Folder' => $this->FolderName,
|
||||
'FolderHash' => $this->FolderHash,
|
||||
'FolderInfo' => $this->FolderInfo,
|
||||
'UidNext' => $this->UidNext,
|
||||
'ThreadUid' => $this->ThreadUid,
|
||||
'NewMessages' => $this->NewMessages,
|
||||
'Filtered' => $this->Filtered,
|
||||
|
|
|
|||
|
|
@ -862,6 +862,9 @@ class Actions
|
|||
$aResult['AllowDraftAutosave'] = (bool)$oSettings->GetConf('AllowDraftAutosave', $aResult['AllowDraftAutosave']);
|
||||
$aResult['AutoLogout'] = (int)$oSettings->GetConf('AutoLogout', $aResult['AutoLogout']);
|
||||
$aResult['Layout'] = (int)$oSettings->GetConf('Layout', $aResult['Layout']);
|
||||
$aResult['Resizer4Width'] = (int)$oSettings->GetConf('Resizer4Width', 0);
|
||||
$aResult['Resizer5Width'] = (int)$oSettings->GetConf('Resizer5Width', 0);
|
||||
$aResult['Resizer5Height'] = (int)$oSettings->GetConf('Resizer5Height', 0);
|
||||
|
||||
if (!$this->GetCapa(Enumerations\Capa::AUTOLOGOUT)) {
|
||||
$aResult['AutoLogout'] = 0;
|
||||
|
|
@ -1165,7 +1168,6 @@ class Actions
|
|||
'Kolab' => false, // See Kolab plugin
|
||||
'MessageActions' => (bool) $oConfig->Get('capa', 'message_actions', true),
|
||||
'OpenPGP' => (bool) $oConfig->Get('security', 'openpgp', false),
|
||||
'Prefetch' => (bool) $oConfig->Get('labs', 'allow_prefetch', false),
|
||||
'Quota' => (bool) $oConfig->Get('capa', 'quota', true),
|
||||
'Search' => (bool) $oConfig->Get('capa', 'search', true),
|
||||
'SearchAdv' => (bool) $oConfig->Get('capa', 'search', true) && $oConfig->Get('capa', 'search_adv', true),
|
||||
|
|
|
|||
|
|
@ -424,8 +424,6 @@ trait Folders
|
|||
(int) $this->GetActionParam('UidNext', 0),
|
||||
new \MailSo\Imap\SequenceSet($this->GetActionParam('FlagsUids', []))
|
||||
);
|
||||
$aInboxInformation['Flags'] = $aInboxInformation['MessagesFlags'];
|
||||
unset($aInboxInformation['MessagesFlags']);
|
||||
return $this->DefaultResponse(__FUNCTION__, $aInboxInformation);
|
||||
}
|
||||
catch (\Throwable $oException)
|
||||
|
|
@ -459,8 +457,8 @@ trait Folders
|
|||
$aResult[] = [
|
||||
'Folder' => $aInboxInformation['Folder'],
|
||||
'Hash' => $aInboxInformation['Hash'],
|
||||
'MessageCount' => $aInboxInformation['MessageCount'],
|
||||
'MessageUnseenCount' => $aInboxInformation['MessageUnseenCount'],
|
||||
'totalEmails' => $aInboxInformation['totalEmails'],
|
||||
'unreadEmails' => $aInboxInformation['unreadEmails'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,7 +155,6 @@ trait Messages
|
|||
|
||||
$sSentFolder = $this->GetActionParam('SaveFolder', '');
|
||||
$aDraftInfo = $this->GetActionParam('DraftInfo', null);
|
||||
$bDsn = '1' === (string) $this->GetActionParam('Dsn', '0');
|
||||
|
||||
$oMessage = $this->buildMessage($oAccount, false);
|
||||
|
||||
|
|
@ -173,6 +172,7 @@ trait Messages
|
|||
|
||||
if (false !== $iMessageStreamSize)
|
||||
{
|
||||
$bDsn = !empty($this->GetActionParam('Dsn', 0));
|
||||
$this->smtpSendMessage($oAccount, $oMessage, $rMessageStream, $iMessageStreamSize, $bDsn, true);
|
||||
|
||||
if (\is_array($aDraftInfo) && 3 === \count($aDraftInfo))
|
||||
|
|
@ -411,6 +411,11 @@ trait Messages
|
|||
return $this->messageSetFlag(MessageFlag::FLAGGED, __FUNCTION__, true);
|
||||
}
|
||||
|
||||
public function DoMessageSetKeyword() : array
|
||||
{
|
||||
return $this->messageSetFlag($this->GetActionParam('Keyword', ''), __FUNCTION__, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \MailSo\Base\Exceptions\Exception
|
||||
*/
|
||||
|
|
@ -479,18 +484,6 @@ trait Messages
|
|||
throw new ClientException(Notifications::CantDeleteMessage, $oException);
|
||||
}
|
||||
|
||||
if ($this->Config()->Get('labs', 'use_imap_unselect', true))
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->MailClient()->FolderUnselect();
|
||||
}
|
||||
catch (\Throwable $oException)
|
||||
{
|
||||
unset($oException);
|
||||
}
|
||||
}
|
||||
|
||||
$sHash = '';
|
||||
try
|
||||
{
|
||||
|
|
@ -559,18 +552,6 @@ trait Messages
|
|||
throw new ClientException(Notifications::CantMoveMessage, $oException);
|
||||
}
|
||||
|
||||
if ($this->Config()->Get('labs', 'use_imap_unselect', true))
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->MailClient()->FolderUnselect();
|
||||
}
|
||||
catch (\Throwable $oException)
|
||||
{
|
||||
unset($oException);
|
||||
}
|
||||
}
|
||||
|
||||
$sHash = '';
|
||||
try
|
||||
{
|
||||
|
|
@ -624,7 +605,7 @@ trait Messages
|
|||
{
|
||||
if ($aValues = \RainLoop\Utils::DecodeKeyValuesQ($sAttachment))
|
||||
{
|
||||
$sFolder = isset($aValues['Folder']) ? $aValues['Folder'] : '';
|
||||
$sFolder = isset($aValues['Folder']) ? (string) $aValues['Folder'] : '';
|
||||
$iUid = isset($aValues['Uid']) ? (int) $aValues['Uid'] : 0;
|
||||
$sMimeIndex = isset($aValues['MimeIndex']) ? (string) $aValues['MimeIndex'] : '';
|
||||
|
||||
|
|
@ -982,7 +963,7 @@ trait Messages
|
|||
private function buildReadReceiptMessage(Account $oAccount) : \MailSo\Mime\Message
|
||||
{
|
||||
$sReadReceipt = $this->GetActionParam('ReadReceipt', '');
|
||||
$sSubject = $this->GetActionParam('Subject', '');
|
||||
$sSubject = $this->GetActionParam('subject', '');
|
||||
$sText = $this->GetActionParam('Text', '');
|
||||
|
||||
$oIdentity = $this->GetIdentityByID($oAccount, '', true);
|
||||
|
|
@ -1074,17 +1055,17 @@ trait Messages
|
|||
$oMessage->SetReplyTo($oReplyTo);
|
||||
}
|
||||
|
||||
if ('1' === $this->GetActionParam('ReadReceiptRequest', '0')) {
|
||||
if (!empty($this->GetActionParam('ReadReceiptRequest', 0))) {
|
||||
// Read Receipts Reference Main Account Email, Not Identities #147
|
||||
// $oMessage->SetReadReceipt(($oFromIdentity ?: $oAccount)->Email());
|
||||
$oMessage->SetReadReceipt($oFrom->GetEmail());
|
||||
}
|
||||
|
||||
if ('1' === $this->GetActionParam('MarkAsImportant', '0')) {
|
||||
if (!empty($this->GetActionParam('MarkAsImportant', 0))) {
|
||||
$oMessage->SetPriority(\MailSo\Mime\Enumerations\MessagePriority::HIGH);
|
||||
}
|
||||
|
||||
$oMessage->SetSubject($this->GetActionParam('Subject', ''));
|
||||
$oMessage->SetSubject($this->GetActionParam('subject', ''));
|
||||
|
||||
$oToEmails = new \MailSo\Mime\EmailCollection($this->GetActionParam('To', ''));
|
||||
if ($oToEmails->count()) {
|
||||
|
|
|
|||
|
|
@ -14,15 +14,14 @@ trait Raw
|
|||
$sRawKey = (string) $this->GetActionParam('RawKey', '');
|
||||
$aValues = $this->getDecodedRawKeyValue($sRawKey);
|
||||
|
||||
$sFolder = isset($aValues['Folder']) ? $aValues['Folder'] : '';
|
||||
$iUid = (isset($aValues['Uid']) ? (int) $aValues['Uid'] : 0);
|
||||
$sMimeIndex = (string) (isset($aValues['MimeIndex']) ? $aValues['MimeIndex'] : '');
|
||||
$sFolder = isset($aValues['Folder']) ? (string) $aValues['Folder'] : '';
|
||||
$iUid = isset($aValues['Uid']) ? (int) $aValues['Uid'] : 0;
|
||||
$sMimeIndex = isset($aValues['MimeIndex']) ? (string) $aValues['MimeIndex'] : '';
|
||||
|
||||
\header('Content-Type: text/plain');
|
||||
|
||||
return $this->MailClient()->MessageMimeStream(function ($rResource) {
|
||||
if (\is_resource($rResource))
|
||||
{
|
||||
if (\is_resource($rResource)) {
|
||||
\MailSo\Base\Utils::FpassthruWithTimeLimitReset($rResource);
|
||||
}
|
||||
}, $sFolder, $iUid, $sMimeIndex);
|
||||
|
|
@ -132,7 +131,7 @@ trait Raw
|
|||
$bIsRangeRequest = true;
|
||||
}
|
||||
|
||||
$sFolder = isset($aValues['Folder']) ? $aValues['Folder'] : '';
|
||||
$sFolder = isset($aValues['Folder']) ? (string) $aValues['Folder'] : '';
|
||||
$iUid = isset($aValues['Uid']) ? (int) $aValues['Uid'] : 0;
|
||||
$sMimeIndex = isset($aValues['MimeIndex']) ? (string) $aValues['MimeIndex'] : '';
|
||||
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ trait Response
|
|||
$mResult[$prop] = $this->responseObject($mResult[$prop], $sParent);
|
||||
}
|
||||
|
||||
$sSubject = $mResult['Subject'];
|
||||
$sSubject = $mResult['subject'];
|
||||
$mResult['Hash'] = \md5($mResult['Folder'].$mResult['Uid']);
|
||||
$mResult['RequestHash'] = Utils::EncodeKeyValuesQ(array(
|
||||
'V' => APP_VERSION,
|
||||
|
|
@ -199,6 +199,11 @@ trait Response
|
|||
if (false !== $key) {
|
||||
$mResult['Flags'][$key] = '$mdnsent';
|
||||
}
|
||||
// KMail
|
||||
$key = \array_search('$replied', $mResult['Flags']);
|
||||
if (false !== $key) {
|
||||
$mResult['Flags'][$key] = '\\answered';
|
||||
}
|
||||
|
||||
$mResult['Flags'] = \array_unique($mResult['Flags']);
|
||||
|
||||
|
|
@ -266,21 +271,19 @@ trait Response
|
|||
|
||||
if ($mResponse instanceof \MailSo\Mail\Folder)
|
||||
{
|
||||
$aExtended = null;
|
||||
$aResult = $mResponse->jsonSerialize();
|
||||
|
||||
$aStatus = $mResponse->Status();
|
||||
if ($aStatus && isset($aStatus['MESSAGES'], $aStatus['UNSEEN'], $aStatus['UIDNEXT'])) {
|
||||
$aExtended = array(
|
||||
'MessageCount' => (int) $aStatus['MESSAGES'],
|
||||
'MessageUnseenCount' => (int) $aStatus['UNSEEN'],
|
||||
'UidNext' => (int) $aStatus['UIDNEXT'],
|
||||
'Hash' => $this->MailClient()->GenerateFolderHash(
|
||||
$mResponse->FullName(), $aStatus['MESSAGES'], $aStatus['UIDNEXT'],
|
||||
empty($aStatus['HIGHESTMODSEQ']) ? 0 : $aStatus['HIGHESTMODSEQ'])
|
||||
if ($aStatus && isset($aStatus['MESSAGES'], $aStatus['UIDNEXT'])) {
|
||||
$aResult['Hash'] = $this->MailClient()->GenerateFolderHash(
|
||||
$mResponse->FullName(),
|
||||
$aStatus['MESSAGES'],
|
||||
$aStatus['UIDNEXT'],
|
||||
empty($aStatus['HIGHESTMODSEQ']) ? 0 : $aStatus['HIGHESTMODSEQ']
|
||||
);
|
||||
}
|
||||
|
||||
if (null === $this->aCheckableFolder)
|
||||
{
|
||||
if (null === $this->aCheckableFolder) {
|
||||
$aCheckable = \json_decode(
|
||||
$this->SettingsProvider(true)
|
||||
->Load($this->getAccountFromToken())
|
||||
|
|
@ -288,14 +291,9 @@ trait Response
|
|||
);
|
||||
$this->aCheckableFolder = \is_array($aCheckable) ? $aCheckable : array();
|
||||
}
|
||||
$aResult['Checkable'] = \in_array($mResponse->FullName(), $this->aCheckableFolder);
|
||||
|
||||
return \array_merge(
|
||||
$mResponse->jsonSerialize(),
|
||||
array(
|
||||
'Checkable' => \in_array($mResponse->FullName(), $this->aCheckableFolder),
|
||||
'Extended' => $aExtended,
|
||||
)
|
||||
);
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
if ($mResponse instanceof \MailSo\Base\Collection)
|
||||
|
|
|
|||
|
|
@ -342,6 +342,10 @@ trait User
|
|||
$this->setSettingsFromParams($oSettings, 'AutoLogout', 'int');
|
||||
$this->setSettingsFromParams($oSettings, 'MessageReadDelay', 'int');
|
||||
|
||||
$this->setSettingsFromParams($oSettings, 'Resizer4Width', 'int');
|
||||
$this->setSettingsFromParams($oSettings, 'Resizer5Width', 'int');
|
||||
$this->setSettingsFromParams($oSettings, 'Resizer5Height', 'int');
|
||||
|
||||
$this->setSettingsFromParams($oSettingsLocal, 'UseThreads', 'bool');
|
||||
$this->setSettingsFromParams($oSettingsLocal, 'ReplySameFolder', 'bool');
|
||||
$this->setSettingsFromParams($oSettingsLocal, 'HideUnsubscribed', 'bool');
|
||||
|
|
@ -476,12 +480,12 @@ trait User
|
|||
{
|
||||
$aValues = $this->getDecodedRawKeyValue($sHash);
|
||||
|
||||
$sFolder = isset($aValues['Folder']) ? $aValues['Folder'] : '';
|
||||
$sFolder = isset($aValues['Folder']) ? (string) $aValues['Folder'] : '';
|
||||
$iUid = isset($aValues['Uid']) ? (int) $aValues['Uid'] : 0;
|
||||
$sMimeIndex = (string) isset($aValues['MimeIndex']) ? $aValues['MimeIndex'] : '';
|
||||
$sMimeIndex = isset($aValues['MimeIndex']) ? (string) $aValues['MimeIndex'] : '';
|
||||
|
||||
$sContentTypeIn = (string) isset($aValues['MimeType']) ? $aValues['MimeType'] : '';
|
||||
$sFileNameIn = (string) isset($aValues['FileName']) ? $aValues['FileName'] : '';
|
||||
$sContentTypeIn = isset($aValues['MimeType']) ? (string) $aValues['MimeType'] : '';
|
||||
$sFileNameIn = isset($aValues['FileName']) ? (string) $aValues['FileName'] : '';
|
||||
|
||||
$oFileProvider = $this->FilesProvider();
|
||||
|
||||
|
|
|
|||
|
|
@ -337,7 +337,6 @@ Enables caching in the system'),
|
|||
),
|
||||
|
||||
'labs' => array(
|
||||
'allow_prefetch' => array(false),
|
||||
'cache_system_data' => array(true),
|
||||
'date_from_headers' => array(true),
|
||||
'autocreate_system_folders' => array(false),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ class Capa
|
|||
{
|
||||
const GNUPG = 'GnuPG';
|
||||
const OPEN_PGP = 'OpenPGP';
|
||||
const PREFETCH = 'Prefetch';
|
||||
const THEMES = 'Themes';
|
||||
const USER_BACKGROUND = 'UserBackground';
|
||||
const SIEVE = 'Sieve';
|
||||
|
|
|
|||
|
|
@ -9,45 +9,35 @@ class Helper
|
|||
$sFoundValue = '';
|
||||
|
||||
$sString = \trim($sString);
|
||||
if ('' === $sString)
|
||||
{
|
||||
if ('' === $sString) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sWildcardValues = \trim($sWildcardValues);
|
||||
if ('' === $sWildcardValues)
|
||||
{
|
||||
if ('' === $sWildcardValues) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ('*' === $sWildcardValues)
|
||||
{
|
||||
if ('*' === $sWildcardValues) {
|
||||
$sFoundValue = '*';
|
||||
return true;
|
||||
}
|
||||
|
||||
$sWildcardValues = \preg_replace('/[*]+/', '*', \preg_replace('/[\s,;]+/', ' ', $sWildcardValues));
|
||||
$aWildcardValues = \explode(' ', $sWildcardValues);
|
||||
$aWildcardValues = \preg_split('/[\\s,;]+/', \preg_replace('/\\*+/', '*', $sWildcardValues));
|
||||
|
||||
foreach ($aWildcardValues as $sItem)
|
||||
{
|
||||
if (false === \strpos($sItem, '*'))
|
||||
{
|
||||
if ($sString === $sItem)
|
||||
{
|
||||
foreach ($aWildcardValues as $sItem) {
|
||||
if (false === \strpos($sItem, '*')) {
|
||||
if ($sString === $sItem) {
|
||||
$sFoundValue = $sItem;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
$aItem = \explode('*', $sItem);
|
||||
$aItem = \array_map(function ($sItem) {
|
||||
return \preg_quote($sItem, '/');
|
||||
}, $aItem);
|
||||
|
||||
if (\preg_match('/'.\implode('.*', $aItem).'/', $sString))
|
||||
{
|
||||
if (\preg_match('/'.\implode('.*', $aItem).'/', $sString)) {
|
||||
$sFoundValue = $sItem;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,18 +79,18 @@ class Domain extends AbstractProvider
|
|||
$sIncHost = (string) $oActions->GetActionParam('IncHost', '');
|
||||
$iIncPort = (int) $oActions->GetActionParam('IncPort', 143);
|
||||
$iIncSecure = (int) $oActions->GetActionParam('IncSecure', \MailSo\Net\Enumerations\ConnectionSecurityType::NONE);
|
||||
$bIncShortLogin = '1' === (string) $oActions->GetActionParam('IncShortLogin', '0');
|
||||
$bUseSieve = '1' === (string) $oActions->GetActionParam('UseSieve', '0');
|
||||
$bIncShortLogin = !empty($oActions->GetActionParam('IncShortLogin', 0));
|
||||
$bUseSieve = !empty($oActions->GetActionParam('UseSieve', 0));
|
||||
$sSieveHost = (string) $oActions->GetActionParam('SieveHost', '');
|
||||
$iSievePort = (int) $oActions->GetActionParam('SievePort', 4190);
|
||||
$iSieveSecure = (int) $oActions->GetActionParam('SieveSecure', \MailSo\Net\Enumerations\ConnectionSecurityType::NONE);
|
||||
$sOutHost = (string) $oActions->GetActionParam('OutHost', '');
|
||||
$iOutPort = (int) $oActions->GetActionParam('OutPort', 25);
|
||||
$iOutSecure = (int) $oActions->GetActionParam('OutSecure', \MailSo\Net\Enumerations\ConnectionSecurityType::NONE);
|
||||
$bOutShortLogin = '1' === (string) $oActions->GetActionParam('OutShortLogin', '0');
|
||||
$bOutAuth = '1' === (string) $oActions->GetActionParam('OutAuth', '1');
|
||||
$bOutSetSender = '1' === (string) $oActions->GetActionParam('OutSetSender', '0');
|
||||
$bOutUsePhpMail = '1' === (string) $oActions->GetActionParam('OutUsePhpMail', '0');
|
||||
$bOutShortLogin = !empty($oActions->GetActionParam('OutShortLogin', 0));
|
||||
$bOutAuth = !empty($oActions->GetActionParam('OutAuth', 1));
|
||||
$bOutSetSender = !empty($oActions->GetActionParam('OutSetSender', 0));
|
||||
$bOutUsePhpMail = !empty($oActions->GetActionParam('OutUsePhpMail', 0));
|
||||
$sWhiteList = (string) $oActions->GetActionParam('WhiteList', '');
|
||||
|
||||
$oDomain = $sNameForTest ? null : $this->Load($sName);
|
||||
|
|
|
|||
|
|
@ -82,18 +82,18 @@ class DefaultDomain implements DomainInterface
|
|||
{
|
||||
$mResult = null;
|
||||
|
||||
$sDisabled = '';
|
||||
$aDisabled = [];
|
||||
$sFoundValue = '';
|
||||
|
||||
$sRealFileName = $this->codeFileName($sName);
|
||||
|
||||
if (\file_exists($this->sDomainPath.'/disabled'))
|
||||
{
|
||||
$sDisabled = \file_get_contents($this->sDomainPath.'/disabled');
|
||||
if (\file_exists($this->sDomainPath.'/disabled')) {
|
||||
$aDisabled = \explode(',', \file_get_contents($this->sDomainPath.'/disabled'));
|
||||
}
|
||||
$bCheckDisabled = $bCheckDisabled && 0 < \count($aDisabled);
|
||||
|
||||
if (\file_exists($this->sDomainPath.'/'.$sRealFileName.'.ini') &&
|
||||
(!$bCheckDisabled || 0 === \strlen($sDisabled) || false === \strpos(','.$sDisabled.',', ','.\MailSo\Base\Utils::IdnToAscii($sName, true).',')))
|
||||
(!$bCheckDisabled || !\in_array(\MailSo\Base\Utils::IdnToAscii($sName, true), $aDisabled)))
|
||||
{
|
||||
$aDomain = \parse_ini_file($this->sDomainPath.'/'.$sRealFileName.'.ini') ?: array();
|
||||
// if ($bCheckAliases && !empty($aDomain['alias']))
|
||||
|
|
@ -109,7 +109,7 @@ class DefaultDomain implements DomainInterface
|
|||
$mResult = \RainLoop\Model\Domain::NewInstanceFromDomainConfigArray($sName, $aDomain);
|
||||
}
|
||||
else if ($bCheckAliases && \file_exists($this->sDomainPath.'/'.$sRealFileName.'.alias') &&
|
||||
(!$bCheckDisabled || 0 === \strlen($sDisabled) || false === \strpos(','.$sDisabled.',', ','.\MailSo\Base\Utils::IdnToAscii($sName, true).',')))
|
||||
(!$bCheckDisabled || !\in_array(\MailSo\Base\Utils::IdnToAscii($sName, true), $aDisabled)))
|
||||
{
|
||||
$sAlias = \trim(\file_get_contents($this->sDomainPath.'/'.$sRealFileName.'.alias'));
|
||||
if (!empty($sAlias))
|
||||
|
|
@ -126,16 +126,12 @@ class DefaultDomain implements DomainInterface
|
|||
else if ($bFindWithWildCard)
|
||||
{
|
||||
$sNames = $this->getWildcardDomainsLine();
|
||||
if (\strlen($sNames))
|
||||
{
|
||||
if (\RainLoop\Plugins\Helper::ValidateWildcardValues(
|
||||
\MailSo\Base\Utils::IdnToUtf8($sName, true), $sNames, $sFoundValue) && \strlen($sFoundValue))
|
||||
{
|
||||
if (!$bCheckDisabled || 0 === \strlen($sDisabled) || false === \strpos(','.$sDisabled.',', ','.$sFoundValue.','))
|
||||
{
|
||||
$mResult = $this->Load($sFoundValue, false);
|
||||
}
|
||||
}
|
||||
if (\strlen($sNames)
|
||||
&& \RainLoop\Plugins\Helper::ValidateWildcardValues(\MailSo\Base\Utils::IdnToUtf8($sName, true), $sNames, $sFoundValue)
|
||||
&& \strlen($sFoundValue)
|
||||
&& (!$bCheckDisabled || !\in_array($sFoundValue, $aDisabled))
|
||||
) {
|
||||
$mResult = $this->Load($sFoundValue, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -186,7 +186,9 @@ class ServiceActions
|
|||
|
||||
$this->Plugins()->RunHook('filter.json-response', array($sAction, &$aResponseItem));
|
||||
|
||||
\header('Content-Type: application/json; charset=utf-8');
|
||||
if (!\headers_sent()) {
|
||||
\header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
$sResult = \MailSo\Base\Utils::Php2js($aResponseItem, $this->Logger());
|
||||
|
||||
|
|
@ -765,8 +767,8 @@ class ServiceActions
|
|||
$oAccount = null;
|
||||
|
||||
if ($this->oActions->Config()->Get('labs', 'allow_external_login', false)) {
|
||||
$sEmail = \trim($this->oHttp->GetRequest('Email', ''));
|
||||
$sPassword = $this->oHttp->GetRequest('Password', '');
|
||||
$sEmail = \trim($_POST['Email']);
|
||||
$sPassword = $_POST['Password'];
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -783,7 +785,7 @@ class ServiceActions
|
|||
}
|
||||
}
|
||||
|
||||
if ('json' === \strtolower($this->oHttp->GetRequest('Output', ''))) {
|
||||
if ('json' === \strtolower($_POST['Output'])) {
|
||||
\header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$aResult = array(
|
||||
|
|
|
|||
|
|
@ -35,12 +35,12 @@ class Utils
|
|||
);
|
||||
}
|
||||
|
||||
public static function DecodeKeyValuesQ(string $sEncodedValues, string $sCustomKey = '') : ?array
|
||||
public static function DecodeKeyValuesQ(string $sEncodedValues, string $sCustomKey = '') : array
|
||||
{
|
||||
return \SnappyMail\Crypt::DecryptUrlSafe(
|
||||
$sEncodedValues,
|
||||
\sha1(APP_SALT.$sCustomKey.'Q'.static::GetSessionToken(false))
|
||||
) ?: null;
|
||||
) ?: array();
|
||||
}
|
||||
|
||||
public static function GetSessionToken(bool $generate = true) : ?string
|
||||
|
|
@ -99,43 +99,68 @@ class Utils
|
|||
*/
|
||||
public static function GetCookie(string $sName, $mDefault = null)
|
||||
{
|
||||
return isset($_COOKIE[$sName]) ? $_COOKIE[$sName] : $mDefault;
|
||||
if (isset($_COOKIE[$sName])) {
|
||||
$aParts = [];
|
||||
foreach (\array_keys($_COOKIE) as $sCookieName) {
|
||||
if (\strtok($sCookieName, '~') === $sName) {
|
||||
$aParts[$sCookieName] = $_COOKIE[$sCookieName];
|
||||
}
|
||||
}
|
||||
\ksort($aParts);
|
||||
return \implode('', $aParts);
|
||||
}
|
||||
return $mDefault;
|
||||
}
|
||||
|
||||
public static function GetSecureCookie(string $sName)
|
||||
{
|
||||
return isset($_COOKIE[$sName]) && 1024 > \strlen($_COOKIE[$sName])
|
||||
? \SnappyMail\Crypt::DecryptFromJSON(\MailSo\Base\Utils::UrlSafeBase64Decode($_COOKIE[$sName]))
|
||||
return isset($_COOKIE[$sName])
|
||||
? \SnappyMail\Crypt::DecryptFromJSON(\MailSo\Base\Utils::UrlSafeBase64Decode(static::GetCookie($sName)))
|
||||
: null;
|
||||
}
|
||||
|
||||
public static function SetCookie(string $sName, string $sValue = '', int $iExpire = 0, bool $bHttpOnly = true)
|
||||
{
|
||||
$sPath = static::$CookieDefaultPath;
|
||||
$sPath = $sPath && \strlen($sPath) ? $sPath : '/';
|
||||
$_COOKIE[$sName] = $sValue;
|
||||
\setcookie($sName, $sValue, array(
|
||||
'expires' => $iExpire,
|
||||
'path' => $sPath && \strlen($sPath) ? $sPath : '/',
|
||||
// 'domain' => $sDomain,
|
||||
'secure' => isset($_SERVER['HTTPS']) || static::$CookieDefaultSecure,
|
||||
'httponly' => $bHttpOnly,
|
||||
'samesite' => 'Strict'
|
||||
));
|
||||
// https://github.com/the-djmaze/snappymail/issues/451
|
||||
// The 4K browser limit is for the entire cookie, including name, value, expiry date etc.
|
||||
$iMaxSize = 4000 - \strlen($sPath . $sName);
|
||||
/*
|
||||
if ($iMaxSize < \strlen($sValue)) {
|
||||
throw new \Exception("Cookie '{$sName}' value too long");
|
||||
}
|
||||
*/
|
||||
foreach (\str_split($sValue, $iMaxSize) as $i => $sPart) {
|
||||
\setcookie($i ? "{$sName}~{$i}" : $sName, $sPart, array(
|
||||
'expires' => $iExpire,
|
||||
'path' => $sPath,
|
||||
// 'domain' => $sDomain,
|
||||
'secure' => isset($_SERVER['HTTPS']) || static::$CookieDefaultSecure,
|
||||
'httponly' => $bHttpOnly,
|
||||
'samesite' => 'Strict'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public static function ClearCookie(string $sName)
|
||||
{
|
||||
if (isset($_COOKIE[$sName])) {
|
||||
$sPath = static::$CookieDefaultPath;
|
||||
unset($_COOKIE[$sName]);
|
||||
\setcookie($sName, '', array(
|
||||
'expires' => \time() - 3600 * 24 * 30,
|
||||
'path' => $sPath && \strlen($sPath) ? $sPath : '/',
|
||||
// 'domain' => null,
|
||||
'secure' => isset($_SERVER['HTTPS']) || static::$CookieDefaultSecure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict'
|
||||
));
|
||||
foreach (\array_keys($_COOKIE) as $sCookieName) {
|
||||
if (\strtok($sCookieName, '~') === $sName) {
|
||||
unset($_COOKIE[$sCookieName]);
|
||||
\setcookie($sCookieName, '', array(
|
||||
'expires' => \time() - 3600 * 24 * 30,
|
||||
'path' => $sPath && \strlen($sPath) ? $sPath : '/',
|
||||
// 'domain' => null,
|
||||
'secure' => isset($_SERVER['HTTPS']) || static::$CookieDefaultSecure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ class Component extends Node
|
|||
$result = [];
|
||||
foreach ($this->children as $childGroup) {
|
||||
foreach ($childGroup as $child) {
|
||||
if ($child instanceof Property && strtoupper($child->group) === $group) {
|
||||
if ($child instanceof Property && $child->group && strtoupper($child->group) === $group) {
|
||||
$result[] = $child;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -291,6 +291,11 @@ class VCard extends VObject\Document
|
|||
$this->FN = (string) $this->ORG;
|
||||
$repaired = true;
|
||||
|
||||
// Otherwise, the NICKNAME property may work
|
||||
} elseif (isset($this->NICKNAME)) {
|
||||
$this->FN = (string) $this->NICKNAME;
|
||||
$repaired = true;
|
||||
|
||||
// Otherwise, the EMAIL property may work
|
||||
} elseif (isset($this->EMAIL)) {
|
||||
$this->FN = (string) $this->EMAIL;
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@ class Parameter extends Node
|
|||
*/
|
||||
public function xmlSerialize(Xml\Writer $writer)
|
||||
{
|
||||
foreach (is_array($this->value) ? $this->value : explode(',', $this->value) as $value) {
|
||||
foreach (explode(',', $this->value) as $value) {
|
||||
$writer->writeElement('text', $value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,11 @@ class CalAddress extends Text
|
|||
return $input;
|
||||
}
|
||||
list($schema, $everythingElse) = explode(':', $input, 2);
|
||||
$schema = strtolower($schema);
|
||||
if ('mailto' === $schema) {
|
||||
$everythingElse = strtolower($everythingElse);
|
||||
}
|
||||
|
||||
return strtolower($schema).':'.$everythingElse;
|
||||
return $schema.':'.$everythingElse;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,23 +40,11 @@ class StringUtil
|
|||
*/
|
||||
public static function convertToUTF8($str)
|
||||
{
|
||||
$encoding = mb_detect_encoding($str, ['UTF-8', 'ISO-8859-1', 'WINDOWS-1252'], true);
|
||||
|
||||
switch ($encoding) {
|
||||
case 'ISO-8859-1':
|
||||
$newStr = utf8_encode($str);
|
||||
break;
|
||||
/* Unreachable code. Not sure yet how we can improve this
|
||||
* situation.
|
||||
case 'WINDOWS-1252' :
|
||||
$newStr = iconv('cp1252', 'UTF-8', $str);
|
||||
break;
|
||||
*/
|
||||
default:
|
||||
$newStr = $str;
|
||||
if (!mb_check_encoding($str, 'UTF-8') && mb_check_encoding($str, 'ISO-8859-1')) {
|
||||
$str = mb_convert_encoding($str, 'UTF-8', 'ISO-8859-1');
|
||||
}
|
||||
|
||||
// Removing any control characters
|
||||
return preg_replace('%(?:[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', '', $newStr);
|
||||
return preg_replace('%(?:[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', '', $str);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,5 +14,5 @@ class Version
|
|||
/**
|
||||
* Full version number.
|
||||
*/
|
||||
const VERSION = '4.4.1';
|
||||
const VERSION = '4.4.2';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ abstract class Crypt
|
|||
} catch (\Throwable $e) {
|
||||
\trigger_error(__CLASS__ . "::{$fn}(): " . $e->getMessage());
|
||||
}
|
||||
// \trigger_error(__CLASS__ . '::Decrypt() invalid $data or $key');
|
||||
} else {
|
||||
// \trigger_error(__CLASS__ . '::Decrypt() invalid $data');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ abstract class Stream
|
|||
public static function JSON($data)
|
||||
{
|
||||
echo \MailSo\Base\Utils::Php2js($data) . "\n";
|
||||
\ob_flush();
|
||||
// \ob_flush();
|
||||
\flush();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ abstract class Repository
|
|||
|
||||
public static function getPackagesList() : array
|
||||
{
|
||||
\RainLoop\Api::Actions()->IsAdminLoggined();
|
||||
empty($_ENV['SNAPPYMAIL_INCLUDE_AS_API']) && \RainLoop\Api::Actions()->IsAdminLoggined();
|
||||
|
||||
$bReal = false;
|
||||
$sError = '';
|
||||
|
|
@ -237,11 +237,11 @@ abstract class Repository
|
|||
&& (!\is_file("{$sPath}.phar") || \unlink("{$sPath}.phar"));
|
||||
}
|
||||
|
||||
public static function installPackage(string $sType, string $sId, string $sFile) : bool
|
||||
public static function installPackage(string $sType, string $sId, string $sFile = '') : bool
|
||||
{
|
||||
\RainLoop\Api::Actions()->IsAdminLoggined();
|
||||
empty($_ENV['SNAPPYMAIL_INCLUDE_AS_API']) && \RainLoop\Api::Actions()->IsAdminLoggined();
|
||||
|
||||
\RainLoop\Api::Logger()->Write('Start package install: '.$sFile.' ('.$sType.')', \MailSo\Log\Enumerations\Type::INFO, 'INSTALLER');
|
||||
\RainLoop\Api::Logger()->Write('Start package install: '.$sId.' ('.$sType.')', \MailSo\Log\Enumerations\Type::INFO, 'INSTALLER');
|
||||
|
||||
$sRealFile = '';
|
||||
|
||||
|
|
@ -255,9 +255,9 @@ abstract class Repository
|
|||
if ($sError) {
|
||||
throw new \Exception($sError);
|
||||
}
|
||||
if (isset($aList[$sId]) && $sFile === $aList[$sId]['file']) {
|
||||
$sRealFile = $sFile;
|
||||
$sTmp = static::download($sFile);
|
||||
if (isset($aList[$sId]) && (!$sFile || $sFile === $aList[$sId]['file'])) {
|
||||
$sRealFile = $aList[$sId]['file'];
|
||||
$sTmp = static::download($aList[$sId]['file']);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,14 @@ locale examples: en, en-US, nl, nl-NL
|
|||
2*3ALPHA ; shortest ISO 639 code
|
||||
["-" extlang] ; sometimes followed by extended language subtags
|
||||
|
||||
https://github.com/the-djmaze/snappymail/issues/435
|
||||
|
||||
https://tools.ietf.org/html/rfc1766
|
||||
https://tools.ietf.org/html/rfc3066
|
||||
https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
|
||||
https://en.wikipedia.org/wiki/IETF_language_tag
|
||||
http://www.unc.edu/~rowlett/units/codes/country.htm
|
||||
http://www.w3.org/WAI/ER/IG/ert/iso639.htm
|
||||
http://www.loc.gov/standards/iso639-2/
|
||||
http://www.unicode.org/onlinedat/countries.html
|
||||
https://www.iana.org/assignments/language-subtag-registry
|
||||
|
||||
|
||||
relativetimeformat.js data is based on Unicode CLDR
|
||||
|
|
|
|||
|
|
@ -151,7 +151,17 @@
|
|||
"MESSAGE_VIEW_MOVE_DESC": "Click folder name in the left panel to select the destination.",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "zip تنزيل كـ",
|
||||
"SPAM_SCORE": "Spam Score",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected",
|
||||
"TAGS": "Tags"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Important",
|
||||
"$todo": "To Do",
|
||||
"$label1": "Important",
|
||||
"$label2": "Work",
|
||||
"$label3": "Personal",
|
||||
"$label4": "To Do",
|
||||
"$label5": "Later"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "إعلام بالقراءة (تم عرضها) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -151,7 +151,17 @@
|
|||
"MESSAGE_VIEW_MOVE_DESC": "Изберете цел от папките в лявото поле",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Свали като ZIP файл",
|
||||
"SPAM_SCORE": "Spam Score",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected",
|
||||
"TAGS": "Tags"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Important",
|
||||
"$todo": "To Do",
|
||||
"$label1": "Important",
|
||||
"$label2": "Work",
|
||||
"$label3": "Personal",
|
||||
"$label4": "To Do",
|
||||
"$label5": "Later"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Обратна разписка (видяно) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -151,7 +151,17 @@
|
|||
"MESSAGE_VIEW_MOVE_DESC": "Vyberte cílovou složku v levém panelu.",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Stáhnout jako zip",
|
||||
"SPAM_SCORE": "Spam Score",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected",
|
||||
"TAGS": "Tags"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Important",
|
||||
"$todo": "To Do",
|
||||
"$label1": "Important",
|
||||
"$label2": "Work",
|
||||
"$label3": "Personal",
|
||||
"$label4": "To Do",
|
||||
"$label5": "Later"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Potvrzení o zobrazení zprávy - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -151,7 +151,17 @@
|
|||
"MESSAGE_VIEW_MOVE_DESC": "Vælg modtagemappe i panelet til venstre.",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Hent som zip-fil",
|
||||
"SPAM_SCORE": "Spam Score",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected",
|
||||
"TAGS": "Tags"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Important",
|
||||
"$todo": "To Do",
|
||||
"$label1": "Important",
|
||||
"$label2": "Work",
|
||||
"$label3": "Personal",
|
||||
"$label4": "To Do",
|
||||
"$label5": "Later"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Kvittering for læsning af - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
"TAB_GENERAL": {
|
||||
"LEGEND_INTERFACE": "Oberfläche",
|
||||
"LABEL_LANGUAGE": "Sprache",
|
||||
"LABEL_LANGUAGE_ADMIN": "Sprache (Admin)",
|
||||
"LABEL_LANGUAGE_ADMIN": "Sprache (Adminpanel)",
|
||||
"LABEL_THEME": "Thema",
|
||||
"LABEL_ALLOW_LANGUAGES_ON_SETTINGS": "Sprachauswahl in den Einstellungen zulassen",
|
||||
"LABEL_ALLOW_THEMES_ON_SETTINGS": "Themenauswahl in den Einstellungen zulassen",
|
||||
|
|
@ -40,9 +40,9 @@
|
|||
"LABEL_ATTACHMENT_SIZE_LIMIT": "Größenlimit für Anhänge",
|
||||
"LABEL_ALLOW_ADDITIONAL_ACCOUNTS": "Zusätzliche Konten erlauben",
|
||||
"LABEL_ALLOW_IDENTITIES": "Mehrere Identitäten erlauben",
|
||||
"ALERT_DATA_ACCESS": "Data folder is accessible. Please configure your web server to hide the data folder from external access. Read more here:",
|
||||
"ALERT_DATA_ACCESS": "Auf den \"Data\"-Ordner kann zugegriffen werden. Konfiguriere den Ordner so, dass von extern nicht zugegriffen werden kann. Weitere Informationen:",
|
||||
"ALERT_WARNING": "Warnung!",
|
||||
"HTML_ALERT_WEAK_PASSWORD": "Sie verwenden das Standard-Admin-Passwort.\n<br>\nBitte <strong><a href=\"#\/security\">ändern<\/a><\/strong> Sie\naus Sicherheitsgründen das Passwort jetzt.\n"
|
||||
"HTML_ALERT_WEAK_PASSWORD": "Sie verwenden das Standard-Admin-Passwort.\n<br>\nBitte <strong><a href=\"#\/security\">ändern<\/a><\/strong> Sie\n das Passwort aus Sicherheitsgründen!\n"
|
||||
},
|
||||
"TAB_LOGIN": {
|
||||
"LEGEND_LOGIN_SCREEN": "Anmeldebildschirm",
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
"LEGEND_CONTACTS": "Kontakte",
|
||||
"LEGEND_STORAGE": "Speicher (PDO)",
|
||||
"LABEL_ENABLE_CONTACTS": "Kontakte aktivieren",
|
||||
"LABEL_ALLOW_SYNC": "Kontakte-Synchronisierung erlauben (mit externem CardDAV-Server)",
|
||||
"LABEL_ALLOW_SYNC": "Synchronisierung von Kontakten erlauben (mit externem CardDAV-Server)",
|
||||
"LABEL_STORAGE_TYPE": "Typ",
|
||||
"LABEL_STORAGE_DSN": "DSN",
|
||||
"LABEL_STORAGE_USER": "Benutzer",
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
"LABEL_REPEAT_PASSWORD": "Wiederholen",
|
||||
"BUTTON_UPDATE_PASSWORD": "Passwort aktualisieren",
|
||||
"LEGEND_SSL": "SSL",
|
||||
"LABEL_REQUIRE_VERIFICATION": "Verlangen, dass das verwendete SSL-Zertifikat beglaubigt ist (IMAP\/SMTP)",
|
||||
"LABEL_REQUIRE_VERIFICATION": "Verlangen, dass das verwendete SSL-Zertifikat signiert ist (IMAP\/SMTP)",
|
||||
"LABEL_ALLOW_SELF_SIGNED": "Selbst-signierte Zertifikate erlauben"
|
||||
},
|
||||
"TAB_PACKAGES": {
|
||||
|
|
@ -97,17 +97,17 @@
|
|||
"LEGEND_AVAILABLE_FOR_UPDATE": "Zur Aktualisierung verfügbar",
|
||||
"LEGEND_AVAILABLE_FOR_INSTALLATION": "Zur Installation verfügbar",
|
||||
"LEGEND_INSTALLED_PACKAGES": "Installierte Erweiterungen",
|
||||
"ALERT_CANNOT_ACCESS_REPOSITORY": "Kann momentan nicht auf das Repository zugreifen."
|
||||
"ALERT_CANNOT_ACCESS_REPOSITORY": "Momentan kann nicht auf das Repository zugegriffen werden."
|
||||
},
|
||||
"TAB_ABOUT": {
|
||||
"LEGEND_ABOUT": "Über",
|
||||
"LABEL_TAG_HINT": "Einfacher, moderner und schneller webbasierter E-Mail-Client",
|
||||
"LABEL_ALL_RIGHTS_RESERVED": "Alle Rechte vorbehalten.",
|
||||
"HINT_READ_CHANGE_LOG": "Bitte Lesen Sie das Änderungsprotokoll vor dem Update.",
|
||||
"HINT_READ_CHANGE_LOG": "Bitte Lesen Sie das Changelog vor dem Update.",
|
||||
"HINT_IS_UP_TO_DATE": "SnappyMail ist auf dem neusten Stand.",
|
||||
"HTML_NEW_VERSION": "Neue Version <b>%VERSION%<\/b> verfügbar.",
|
||||
"LABEL_UPDATING": "Aktualisiere",
|
||||
"LABEL_CHECKING": "Prüfe auf Aktualisierungen",
|
||||
"LABEL_CHECKING": "Überprüfe auf Aktualisierungen",
|
||||
"BUTTON_RELEASES": "Releases"
|
||||
},
|
||||
"POPUPS_DOMAIN_ALIAS": {
|
||||
|
|
@ -142,7 +142,7 @@
|
|||
"BUTTON_BACK": "Zurück",
|
||||
"BUTTON_UPDATE": "Aktualisieren",
|
||||
"NEW_DOMAIN_DESC": "Diese Domain Konfiguration wird es dir möglich machen <br>mit <i>%NAME%<\/i> Mailadressen zu arbeiten.",
|
||||
"WHITE_LIST_ALERT": "Liste der User, die Webmail abrufen darf.\nVerwenden Sie Leerzeichen als Trenner.\n"
|
||||
"WHITE_LIST_ALERT": "Liste der User, die Webmail abrufen darf.\nVerwenden Sie Leerzeichen als Trennsymbol.\n"
|
||||
},
|
||||
"POPUPS_PLUGIN": {
|
||||
"TITLE_PLUGIN": "Erweiterung",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"BACK_LINK": "Aktualisieren",
|
||||
"NO_SCRIPT_TITLE": "Diese Anwendung benötigt JavaScript.",
|
||||
"NO_SCRIPT_DESC": "Ihr Browser unterstützt JavaScript nicht.\nAktivieren Sie bitte die JavaScript-Unterstützung in Ihrem Browser und versuchen Sie es erneut.\n",
|
||||
"NO_SCRIPT_DESC": "Ihr Browser unterstützt JavaScript nicht.\nAktivieren Sie die JavaScript-Unterstützung in Ihrem Browser und versuchen Sie es erneut.\n",
|
||||
"NO_COOKIE_TITLE": "Diese Anwendung benötigt Cookies.",
|
||||
"NO_COOKIE_DESC": "Ihr Browser unterstützt Cookies nicht.\nAktivieren Sie bitte die Cookie-Unterstützung in Ihrem Browser und versuchen Sie es erneut.\n",
|
||||
"NO_COOKIE_DESC": "Ihr Browser unterstützt Cookies nicht.\nAktivieren Sie die Cookie-Unterstützung in Ihrem Browser und versuchen Sie es erneut.\n",
|
||||
"BAD_BROWSER_TITLE": "Ihr Browser ist veraltet.",
|
||||
"BAD_BROWSER_DESC": "Um alle Funktionen dieser Anwendung nutzen zu können,\nsollten Sie einen der folgenden Browser herunterladen und installieren\n"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,14 +59,14 @@
|
|||
"LABEL_ADV_DATE_3_MONTHS": "Nicht älter als 3 Monate",
|
||||
"LABEL_ADV_DATE_6_MONTHS": "Nicht älter als 6 Monate",
|
||||
"LABEL_ADV_DATE_YEAR": "Nicht älter als 1 Jahr",
|
||||
"LABEL_ADV_SUBFOLDERS": "Subfolders",
|
||||
"LABEL_ADV_SUBFOLDERS_NONE": "None",
|
||||
"LABEL_ADV_SUBFOLDERS_SUBTREE": "All",
|
||||
"LABEL_ADV_SUBFOLDERS": "Unterordner",
|
||||
"LABEL_ADV_SUBFOLDERS_NONE": "Keine",
|
||||
"LABEL_ADV_SUBFOLDERS_SUBTREE": "Alle",
|
||||
"LABEL_ADV_SUBFOLDERS_SUBTREE_ONE": "One level"
|
||||
},
|
||||
"PREVIEW_POPUP": {
|
||||
"FULLSCREEN": "Vollbild umschalten",
|
||||
"ZOOM": "Herein-\/Herauszoomen",
|
||||
"ZOOM": "Rein-\/Rauszoomen",
|
||||
"CLOSE": "Schließen (Esc)",
|
||||
"GALLERY_PREV": "Zurück (Linke Pfeil-Taste)",
|
||||
"GALLERY_NEXT": "Weiter (Rechte Pfeil-Taste)",
|
||||
|
|
@ -151,7 +151,17 @@
|
|||
"MESSAGE_VIEW_MOVE_DESC": "Klicke auf den Ordnernamen auf der linken Seite, um die Nachricht zu verschieben.",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Als ZIP-Datei herunterladen",
|
||||
"SPAM_SCORE": "Spam-Score",
|
||||
"HAS_VIRUS_WARNING": "WARNUNG: Virus erkannt"
|
||||
"HAS_VIRUS_WARNING": "WARNUNG: Virus erkannt",
|
||||
"TAGS": "Stichworte"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Wichtig",
|
||||
"$todo": "Machen",
|
||||
"$label1": "Wichtig",
|
||||
"$label2": "Arbeit",
|
||||
"$label3": "Persönlich",
|
||||
"$label4": "Machen",
|
||||
"$label5": "Später"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Empfangsbestätigung (angezeigt) - %SUBJECT%",
|
||||
|
|
@ -247,7 +257,7 @@
|
|||
"TITLE_CLEAR_FOLDER": "Alle Nachrichten in diesem Ordner löschen?",
|
||||
"DANGER_DESC_WARNING": "Achtung!",
|
||||
"DANGER_DESC_HTML_1": "Dieser Schritt wird alle Nachrichten im Ordner <strong>%FOLDER%<\/strong> endgültig löschen.",
|
||||
"DANGER_DESC_HTML_2": "Einmal begonnen, kann dieser Vorgang nicht mehr abgebrochen oder beendet werden.",
|
||||
"DANGER_DESC_HTML_2": "Einmal begonnen, kann dieser Vorgang nicht mehr abgebrochen werden.",
|
||||
"TITLE_CLEARING_PROCESS": "Ordner wird gelöscht ..."
|
||||
},
|
||||
"OPENPGP": {
|
||||
|
|
@ -261,11 +271,11 @@
|
|||
"LABEL_SIGN": "Unterschrift",
|
||||
"LABEL_ENCRYPT": "Verschlüsselung",
|
||||
"BUTTON_DECRYPT": "Decrypt",
|
||||
"BUTTON_VERIFY": "Verify",
|
||||
"BUTTON_VERIFY": "Überprüfen",
|
||||
"SIGNED_MESSAGE": "OpenPGP-signierte Nachricht",
|
||||
"ENCRYPTED_MESSAGE": "OpenPGP-verschlüsselte Nachricht",
|
||||
"STORE_IN_GNUPG": "Store on server in GnuPG",
|
||||
"BACKUP_ON_SERVER": "Store (encrypted) on server",
|
||||
"STORE_IN_GNUPG": "Auf dem Server als GnuPG speichern",
|
||||
"BACKUP_ON_SERVER": "Auf dem Server verschlüsselt speichern",
|
||||
"GOOD_SIGNATURE": "Gültige Unterschrift von %USER%",
|
||||
"ERROR": "OpenPGP-Fehler: %ERROR%"
|
||||
},
|
||||
|
|
@ -520,8 +530,8 @@
|
|||
"INVALID_RECIPIENTS": "Ungültige Empfängeradressen",
|
||||
"CANT_SAVE_FILTERS": "Die Filter können nicht gespeichert werden",
|
||||
"CANT_GET_FILTERS": "Die Filter sind nicht verfügbar",
|
||||
"CANT_ACTIVATE_FILTERS_SCRIPT": "Can't activate filters script",
|
||||
"CANT_DELETE_FILTERS_SCRIPT": "Can't delete filters script",
|
||||
"CANT_ACTIVATE_FILTERS_SCRIPT": "Filters-Skript kann nicht aktiviert werden",
|
||||
"CANT_DELETE_FILTERS_SCRIPT": "Filters-Skript kann nicht gelöscht werden",
|
||||
"FILTERS_ARE_NOT_CORRECT": "Die Filter sind fehlerhaft",
|
||||
"CANT_CREATE_FOLDER": "Dieser Ordner kann nicht angelegt werden",
|
||||
"CANT_RENAME_FOLDER": "Dieser Ordner kann nicht umbenannt werden",
|
||||
|
|
|
|||
|
|
@ -151,7 +151,17 @@
|
|||
"MESSAGE_VIEW_MOVE_DESC": "Click folder name in the left panel to select the destination.",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Μεταφόρτωση σαν zip",
|
||||
"SPAM_SCORE": "Spam Score",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected",
|
||||
"TAGS": "Tags"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Important",
|
||||
"$todo": "To Do",
|
||||
"$label1": "Important",
|
||||
"$label2": "Work",
|
||||
"$label3": "Personal",
|
||||
"$label4": "To Do",
|
||||
"$label5": "Later"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Αποδεικτικό Ανάγνωσης (παρουσιάστηκε) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -151,7 +151,17 @@
|
|||
"MESSAGE_VIEW_MOVE_DESC": "Click folder name in the left panel to select the destination.",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Download as zip",
|
||||
"SPAM_SCORE": "Spam Score",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected",
|
||||
"TAGS": "Tags"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Important",
|
||||
"$todo": "To Do",
|
||||
"$label1": "Important",
|
||||
"$label2": "Work",
|
||||
"$label3": "Personal",
|
||||
"$label4": "To Do",
|
||||
"$label5": "Later"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Return Receipt (displayed) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -151,7 +151,17 @@
|
|||
"MESSAGE_VIEW_MOVE_DESC": "Haga clic en el nombre de la carpeta del panel izquierdo para seleccionar el destino.",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Descargar todo (archivo ZIP)",
|
||||
"SPAM_SCORE": "Puntuación de spam",
|
||||
"HAS_VIRUS_WARNING": "ADVERTENCIA: virus detectado"
|
||||
"HAS_VIRUS_WARNING": "ADVERTENCIA: virus detectado",
|
||||
"TAGS": "Tags"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Important",
|
||||
"$todo": "To Do",
|
||||
"$label1": "Important",
|
||||
"$label2": "Work",
|
||||
"$label3": "Personal",
|
||||
"$label4": "To Do",
|
||||
"$label5": "Later"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Acuse de recibo (se visualiza) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -151,7 +151,17 @@
|
|||
"MESSAGE_VIEW_MOVE_DESC": "Click folder name in the left panel to select the destination.",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Laadi alla .zip failina",
|
||||
"SPAM_SCORE": "Spam Score",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected",
|
||||
"TAGS": "Tags"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Important",
|
||||
"$todo": "To Do",
|
||||
"$label1": "Important",
|
||||
"$label2": "Work",
|
||||
"$label3": "Personal",
|
||||
"$label4": "To Do",
|
||||
"$label5": "Later"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Kohaletoimetamise kinnitus - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -151,7 +151,17 @@
|
|||
"MESSAGE_VIEW_MOVE_DESC": "بر روی نام شاخه در پنل سمت راست جهت انتخاب مقصد کلیک کنید",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "دریافت با پسوند zip",
|
||||
"SPAM_SCORE": "Spam Score",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected",
|
||||
"TAGS": "Tags"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Important",
|
||||
"$todo": "To Do",
|
||||
"$label1": "Important",
|
||||
"$label2": "Work",
|
||||
"$label3": "Personal",
|
||||
"$label4": "To Do",
|
||||
"$label5": "Later"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "برگرداندن گیرنده (نمایش داده شد) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -151,7 +151,17 @@
|
|||
"MESSAGE_VIEW_MOVE_DESC": "Click folder name in the left panel to select the destination.",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Lataa zip-tiedostona",
|
||||
"SPAM_SCORE": "Spam Score",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected",
|
||||
"TAGS": "Tags"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Important",
|
||||
"$todo": "To Do",
|
||||
"$label1": "Important",
|
||||
"$label2": "Work",
|
||||
"$label3": "Personal",
|
||||
"$label4": "To Do",
|
||||
"$label5": "Later"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Kuitattu luetuksi - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -151,7 +151,17 @@
|
|||
"MESSAGE_VIEW_MOVE_DESC": "Cliquez sur le nom du dossier dans le panneau de gauche pour sélectionner la destination.",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Télécharger le zip",
|
||||
"SPAM_SCORE": "Score de spam",
|
||||
"HAS_VIRUS_WARNING": "ATTENTION : virus détecté"
|
||||
"HAS_VIRUS_WARNING": "ATTENTION : virus détecté",
|
||||
"TAGS": "Étiquettes"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Important",
|
||||
"$todo": "À faire",
|
||||
"$label1": "Important",
|
||||
"$label2": "Travail",
|
||||
"$label3": "Personnel",
|
||||
"$label4": "À faire",
|
||||
"$label5": "Plus tard"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Accusé de réception (affiché) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -151,7 +151,17 @@
|
|||
"MESSAGE_VIEW_MOVE_DESC": "A cél kiválasztásához a bal oldali panelen kattints a mappa nevére.",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Letöltés zip fájlként",
|
||||
"SPAM_SCORE": "Spam Score",
|
||||
"HAS_VIRUS_WARNING": "FIGYELEM: vírust észleltünk"
|
||||
"HAS_VIRUS_WARNING": "FIGYELEM: vírust észleltünk",
|
||||
"TAGS": "Tags"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Important",
|
||||
"$todo": "To Do",
|
||||
"$label1": "Important",
|
||||
"$label2": "Work",
|
||||
"$label3": "Personal",
|
||||
"$label4": "To Do",
|
||||
"$label5": "Later"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Visszaigazolás (megjelenítve) - %SUBJECT%",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue