Merge branch 'master' into addressbook

# Conflicts:
#	snappymail/v/0.0.0/app/templates/Views/User/PopupsContacts.html
This commit is contained in:
the-djmaze 2022-07-21 14:25:51 +02:00
commit f46e996570
138 changed files with 2587 additions and 1016 deletions

View file

@ -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"

View file

@ -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 |

View file

@ -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'));

View file

@ -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);
}
}
};

View file

@ -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());
}
/**

View file

@ -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();
}

View file

@ -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();
},
/**

View file

@ -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);

View file

@ -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);
}

View file

@ -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] + "')");
});
}
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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) {

View file

@ -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() {

View file

@ -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]);
}

View file

@ -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 &&

View file

@ -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,

View file

@ -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";

View file

@ -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 {

View 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; }

View file

@ -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; }

View file

@ -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;

View file

@ -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 {

View file

@ -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,

View file

@ -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);

View file

@ -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);

View file

@ -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' : ''

View file

@ -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() {

View file

@ -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() })
});
}

View file

@ -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.

View file

@ -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 && \

View file

@ -1 +1 @@
2.16.3
2.17.0

View file

@ -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>

View file

@ -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

View file

@ -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",

View file

@ -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',

View 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;
}
}
}

View 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';
}
}

View file

@ -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

View 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.

View file

@ -0,0 +1 @@
Plugin which allows you to set up LDAP username by email address (when IMAP/SMTP requires it)

View file

@ -0,0 +1 @@
2.0

View 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');
}
}
}

View file

@ -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");
}
}
}

View file

@ -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).';

View file

@ -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();
}
});
}
}

View file

@ -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);
}
}
};
})();
})();

View file

@ -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"

View file

@ -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',

View file

@ -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';

View file

@ -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)

View file

@ -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

View file

@ -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
{

View file

@ -30,4 +30,6 @@ abstract class FolderResponseStatus
const APPENDLIMIT = 'APPENDLIMIT';
// RFC 8474
const MAILBOXID = 'MAILBOXID';
// RFC 9051 IMAP4rev2
const SIZE = 'SIZE';
}

View file

@ -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';

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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]);
}
}
}

View file

@ -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
);
}
}

View file

@ -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;
}

View file

@ -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)
);
}
}

View file

@ -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,

View file

@ -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),

View file

@ -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'],
];
}
}

View file

@ -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()) {

View file

@ -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'] : '';

View file

@ -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)

View file

@ -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();

View file

@ -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),

View file

@ -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';

View file

@ -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;
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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(

View file

@ -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'
));
}
}
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -14,5 +14,5 @@ class Version
/**
* Full version number.
*/
const VERSION = '4.4.1';
const VERSION = '4.4.2';
}

View file

@ -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');
}

View file

@ -42,7 +42,7 @@ abstract class Stream
public static function JSON($data)
{
echo \MailSo\Base\Utils::Php2js($data) . "\n";
\ob_flush();
// \ob_flush();
\flush();
}
}

View file

@ -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']);
}
}

View 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

View file

@ -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%",

View file

@ -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%",

View file

@ -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%",

View file

@ -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%",

View file

@ -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",

View file

@ -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"
}

View file

@ -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",

View file

@ -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%",

View file

@ -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%",

View file

@ -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%",

View file

@ -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%",

View file

@ -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%",

View file

@ -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%",

View file

@ -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%",

View file

@ -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