Merge pull request #1 from cm-schl/rework-on-mailaddress-handling

Rework on mailaddress handling
This commit is contained in:
cm-schl 2023-03-14 14:48:17 +01:00 committed by GitHub
commit 7662889237
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
101 changed files with 896 additions and 770 deletions

View file

@ -316,5 +316,5 @@ dev_email = ""
dev_password = "" dev_password = ""
[version] [version]
current = "2.25.5" current = "2.26.1"
saved = "Sun, 18 Dec 2022 22:10:48 +0000" saved = "Sun, 18 Dec 2022 22:10:48 +0000"

View file

@ -1,3 +1,62 @@
## 2.26.1 2023-02-14
## Added
- Option to allow `<style>` in messages (beta)
- Message in new tab/window was missing BCC
## Changed
- Don't clone message for viewing, so that there is instant interaction with messagelist item
- emailArrayToStringLineHelper() now filters addresses without email address
- Cleanup EmailModel and better email address parsing and handling
- Workaround "server connection error"
[#936](https://github.com/the-djmaze/snappymail/pull/936)
- Cleanup AbstractViewPopup handling
- Italian translation by @lota
[#948](https://github.com/the-djmaze/snappymail/pull/948)
- Replace Element.fromHTML() with createElement()
## Fixed
- New subfolder not visible
[#937](https://github.com/the-djmaze/snappymail/pull/937)
- OpenPGP decrypted attachments `friendlySize` not a function
- Message @media print was broken due to new `display:flex`
- addressparser() didn't handle groups properly
- middleclick in messages list failed when messagesDom not yet initialized
- Cannot open messages that have an email address without an @ in the From header
[#950](https://github.com/the-djmaze/snappymail/pull/950)
- CSS don't display:flex when `[hidden]`
- Default theme handling by @Niveshkrishna
## 2.26.0 2023-02-10
## Added
- Whitelist advanced SPF/DKIM/DMARC valid feature
[#938](https://github.com/the-djmaze/snappymail/pull/938)
## Changed
- \RainLoop\Providers\AddressBook\Utils functions param `Contact` changed to `VCard`
## Fixed
- Issue with themes and Chrome cache
[#188](https://github.com/the-djmaze/snappymail/pull/188)
- Settings panel width due to display:flex
[#940](https://github.com/the-djmaze/snappymail/pull/940)
- Not respecting default theme setting
[#941](https://github.com/the-djmaze/snappymail/pull/941)
- Some files had 0755 instead of 0644
- Some spacing between message "view images" buttons for
[#201](https://github.com/the-djmaze/snappymail/pull/201)
- Whitelist failed when empty or when using `:`
[#938](https://github.com/the-djmaze/snappymail/pull/938)
- Cosmetics of the external images whitelist menu
[#939](https://github.com/the-djmaze/snappymail/pull/939)
- PdoAddressBook ORDER BY deleted DESC to prevent sync and export issues
- Undefined variable $items in upgrade.php
- qq.com not supporting literal-string in search
[#836](https://github.com/the-djmaze/snappymail/pull/836)
## 2.25.5 2023-02-09 ## 2.25.5 2023-02-09
## Added ## Added

View file

@ -141,26 +141,26 @@ RainLoop 1.17 vs SnappyMail
|js/* |RainLoop |Snappy | |js/* |RainLoop |Snappy |
|--------------- |--------: |--------: | |--------------- |--------: |--------: |
|admin.js |2.170.153 | 81.589 | |admin.js |2.170.153 | 81.159 |
|app.js |4.207.787 | 416.123 | |app.js |4.207.787 | 414.949 |
|boot.js | 868.735 | 2.711 | |boot.js | 868.735 | 2.711 |
|libs.js | 658.812 | 192.392 | |libs.js | 658.812 | 192.392 |
|sieve.js | 0 | 85.597 | |sieve.js | 0 | 85.597 |
|polyfills.js | 334.608 | 0 | |polyfills.js | 334.608 | 0 |
|serviceworker.js | 0 | 285 | |serviceworker.js | 0 | 285 |
|TOTAL |8.240.095 | 778.697 | |TOTAL |8.240.095 | 777.093 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: | |--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 256.831 | 40.319 | 73.606 | 13.343 | 60.877 | 11.970 | |admin.min.js | 256.831 | 40.198 | 73.606 | 13.281 | 60.877 | 11.916 |
|app.min.js | 515.367 | 191.764 |139.456 | 64.128 |110.485 | 54.852 | |app.min.js | 515.367 | 190.317 |139.456 | 63.879 |110.485 | 54.792 |
|boot.min.js | 84.659 | 1.600 | 26.998 | 940 | 23.643 | 767 | |boot.min.js | 84.659 | 1.600 | 26.998 | 940 | 23.643 | 767 |
|libs.min.js | 584.772 | 91.434 |180.901 | 33.893 |155.182 | 30.383 | |libs.min.js | 584.772 | 91.434 |180.901 | 33.893 |155.182 | 30.383 |
|sieve.min.js | 0 | 41.566 | 0 | 10.427 | 0 | 9.413 | |sieve.min.js | 0 | 41.566 | 0 | 10.427 | 0 | 9.413 |
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 | |polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|TOTAL user |1.217.635 | 284.798 |358.761 | 98.961 |299.485 | 86.002 | |TOTAL user |1.217.635 | 283.351 |358.761 | 98.712 |299.485 | 85.942 |
|TOTAL user+sieve |1.217.635 | 326.364 |358.761 |109.388 |299.485 | 95.415 | |TOTAL user+sieve |1.217.635 | 324.917 |358.761 |109.139 |299.485 | 95.355 |
|TOTAL admin | 959.099 | 133.353 |292.911 | 48.176 |249.877 | 43.120 | |TOTAL admin | 959.099 | 133.232 |292.911 | 48.114 |249.877 | 43.066 |
For a user its around 70% smaller and faster than traditional RainLoop. For a user its around 70% smaller and faster than traditional RainLoop.
@ -189,12 +189,12 @@ For a user its around 70% smaller and faster than traditional RainLoop.
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli | |css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: | |------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.331 | 81.923 | 46.946 | 17.033 | 14.647 | |app.css | 340.331 | 82.134 | 46.946 | 17.067 | 14.688 |
|app.min.css | 274.947 | 66.171 | 39.647 | 15.062 | 13.240 | |app.min.css | 274.947 | 66.337 | 39.647 | 15.095 | 13.285 |
|boot.css | | 1.326 | | 664 | 545 | |boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 | |boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 30.540 | | 6.984 | 6.093 | |admin.css | | 30.554 | | 6.984 | 6.085 |
|admin.min.css | | 24.738 | | 6.340 | 5.589 | |admin.min.css | | 24.752 | | 6.339 | 5.606 |
### PGP ### PGP
RainLoop uses the old OpenPGP.js v2 RainLoop uses the old OpenPGP.js v2

View file

@ -129,7 +129,7 @@ folderInformation = (folder, list) => {
const oldHash = folderFromCache.hash, const oldHash = folderFromCache.hash,
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails); unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
// folderFromCache.revivePropertiesFromJson(result); // folderFromCache.revivePropertiesFromJson(result);
folderFromCache.expires = Date.now(); folderFromCache.expires = Date.now();
folderFromCache.uidNext = result.uidNext; folderFromCache.uidNext = result.uidNext;
folderFromCache.hash = result.hash; folderFromCache.hash = result.hash;
@ -151,9 +151,11 @@ folderInformation = (folder, list) => {
if (!oldHash || unreadCountChange || result.hash !== oldHash) { if (!oldHash || unreadCountChange || result.hash !== oldHash) {
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) { if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
MessagelistUserStore.reload(); MessagelistUserStore.reload();
/*
} else if (getFolderInboxName() === folderFromCache.fullName) { } else if (getFolderInboxName() === folderFromCache.fullName) {
// Remote.messageList(null, {folder: getFolderFromCacheList(getFolderInboxName())}, true); // Remote.messageList(null, {folder: getFolderFromCacheList(getFolderInboxName())}, true);
Remote.messageList(null, {folder: getFolderInboxName()}, true); Remote.messageList(null, {folder: getFolderInboxName()}, true);
*/
} }
} }
} }

View file

@ -18,7 +18,8 @@ const
SettingsUserStore.collapseBlockquotes() && SettingsUserStore.collapseBlockquotes() &&
// tpl.content.querySelectorAll('blockquote').forEach(node => { // tpl.content.querySelectorAll('blockquote').forEach(node => {
[...tpl.content.querySelectorAll('blockquote')].reverse().forEach(node => { [...tpl.content.querySelectorAll('blockquote')].reverse().forEach(node => {
const el = Element.fromHTML('<details class="sm-bq-switcher"><summary>•••</summary></details>'); const el = createElement('details', {class:'sm-bq-switcher'});
el.innerHTML = '<summary>•••</summary>';
node.replaceWith(el); node.replaceWith(el);
el.append(node); el.append(node);
}); });
@ -87,6 +88,86 @@ const
}); });
} }
return url; return url;
},
/*
Parses given css string, and returns css object
keys as selectors and values are css rules
eliminates all css comments before parsing
@param source css string to be parsed
@return object css
*/
parseCSS = source => {
const css = [];
css.toString = () => css.reduce(
(ret, tmp) =>
ret + tmp.selector + ' {\n'
+ (tmp.type === 'media' ? tmp.subStyles.toString() : tmp.rules)
+ '}\n'
,
''
);
/**
* Given css array, parses it and then for every selector,
* prepends namespace to prevent css collision issues
*/
css.applyNamespace = namespace => css.forEach(obj => {
if (obj.type === 'media') {
obj.subStyles.applyNamespace(namespace);
} else {
obj.selector = obj.selector.split(',').map(selector =>
(namespace + ' .mail-body ' + selector.replace(/\./g, '.msg-'))
.replace(/\sbody/gi, '')
).join(',');
}
});
if (source) {
source = source
// strip comments
.replace(/\/\*[\s\S]*?\*\/|<!--|-->/gi, '')
// strip import statements
.replace(/@import .*?;/gi , '')
// strip keyframe statements
.replace(/((@.*?keyframes [\s\S]*?){([\s\S]*?}\s*?)})/gi, '');
// unified regex to match css & media queries together
let unified = /((\s*?(?:\/\*[\s\S]*?\*\/)?\s*?@media[\s\S]*?){([\s\S]*?)}\s*?})|(([\s\S]*?){([\s\S]*?)})/gi,
arr;
while (true) {
arr = unified.exec(source);
if (arr === null) {
break;
}
let selector = arr[arr[2] === undefined ? 5 : 2].split('\r\n').join('\n').trim()
// Never have more than a single line break in a row
.replace(/\n+/, "\n")
// Remove :root and html
.split(/\s+/g).map(item => item.replace(/^(:root|html)$/, '')).join(' ').trim();
// determine the type
if (selector.includes('@media')) {
// we have a media query
css.push({
selector: selector,
type: 'media',
subStyles: parseCSS(arr[3] + '\n}') //recursively parse media query inner css
});
} else if (selector && !selector.includes('@')) {
// we have standard css
css.push({
selector: selector,
rules: arr[6]
});
}
}
}
return css;
}; };
export const export const
@ -102,7 +183,7 @@ export const
* @param {string} text * @param {string} text
* @returns {string} * @returns {string}
*/ */
cleanHtml = (html, oAttachments) => { cleanHtml = (html, oAttachments, msgId) => {
let aColor; let aColor;
const const
debug = false, // Config()->Get('debug', 'enable', false); debug = false, // Config()->Get('debug', 'enable', false);
@ -159,7 +240,7 @@ export const
'colspan', 'rowspan', 'headers' 'colspan', 'rowspan', 'headers'
], ],
disallowedTags = [ disallowedTags = [
'STYLE','SVG','SCRIPT','TITLE','LINK','BASE','META', 'SVG','SCRIPT','TITLE','LINK','BASE','META',
'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA', 'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA',
'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP' 'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
// Not supported by <template> element // Not supported by <template> element
@ -169,7 +250,15 @@ export const
'A','B','EM','I','SPAN','STRONG' 'A','B','EM','I','SPAN','STRONG'
]; ];
if (SettingsUserStore.allowStyles()) {
allowedAttributes.push('class');
} else {
msgId = 0;
}
tpl.innerHTML = html tpl.innerHTML = html
// Strip Microsoft comments
.replace(/<!--\[if[\s\S]*?endif\]-->/gi, '')
// .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>')) // .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>'))
// Not supported by <template> element // Not supported by <template> element
// .replace(/<!doctype[^>]*>/gi, '') // .replace(/<!doctype[^>]*>/gi, '')
@ -198,6 +287,21 @@ export const
const name = oElement.tagName, const name = oElement.tagName,
oStyle = oElement.style; oStyle = oElement.style;
if ('STYLE' === name) {
let css = msgId ? parseCSS(oElement.textContent) : [];
if (css.length) {
css.applyNamespace(msgId);
css = css.toString();
if (SettingsUserStore.removeColors()) {
css = css.replace(/(background-)?color:[^};]+;?/g, '');
}
oElement.textContent = css;
} else {
oElement.remove();
}
return;
}
// \MailSo\Base\HtmlUtils::ClearTags() // \MailSo\Base\HtmlUtils::ClearTags()
if (disallowedTags.includes(name) if (disallowedTags.includes(name)
|| 'none' == oStyle.display || 'none' == oStyle.display
@ -210,29 +314,23 @@ export const
oElement.remove(); oElement.remove();
return; return;
} }
/*
// Idea to allow CSS
if ('STYLE' === name) {
msgId = '#rl-msg-061eb4d647771be4185943ce91f0039d';
oElement.textContent = oElement.textContent
.replace(/[^{}]+{/g, m => msgId + ' ' + m.replace(',', ', '+msgId+' '))
.replace(/(background-)color:[^};]+/g, '');
return;
}
*/
const aAttrsForRemove = [], const aAttrsForRemove = [],
className = oElement.className,
hasAttribute = name => oElement.hasAttribute(name), hasAttribute = name => oElement.hasAttribute(name),
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '', getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
setAttribute = (name, value) => oElement.setAttribute(name, value), setAttribute = (name, value) => oElement.setAttribute(name, value),
delAttribute = name => oElement.removeAttribute(name); delAttribute = name => oElement.removeAttribute(name);
if ('mail-body' === oElement.className) { if ('mail-body' === className) {
forEachObjectEntry(tasks, (name, cb) => { forEachObjectEntry(tasks, (name, cb) => {
if (hasAttribute(name)) { if (hasAttribute(name)) {
cb(getAttribute(name), oElement); cb(getAttribute(name), oElement);
delAttribute(name); delAttribute(name);
} }
}); });
} else if (msgId && className) {
oElement.className = className.replace(/(^|\s+)/g, '$1msg-');
} }
if (oElement.hasAttributes()) { if (oElement.hasAttributes()) {
@ -258,7 +356,7 @@ export const
delAttribute('width'); delAttribute('width');
} }
value = oStyle.removeProperty('width'); value = oStyle.removeProperty('width');
if (value && !oStyle.maxWidth) { if (parseInt(value,10) && !oStyle.maxWidth) {
oStyle.maxWidth = value; oStyle.maxWidth = value;
oStyle.width = '100%'; oStyle.width = '100%';
} }

View file

@ -4,7 +4,7 @@ import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser'
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals'; import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
import { plainToHtml } from 'Common/Html'; import { plainToHtml } from 'Common/Html';
import { getNotification } from 'Common/Translator'; import { getNotification } from 'Common/Translator';
import { EmailModel } from 'Model/Email'; import { EmailCollectionModel } from 'Model/EmailCollection';
import { MessageModel } from 'Model/Message'; import { MessageModel } from 'Model/Message';
import { MessageUserStore } from 'Stores/User/Message'; import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
@ -158,27 +158,13 @@ mailToHelper = mailToUrl => {
const const
email = mailToUrl[0], email = mailToUrl[0],
params = new URLSearchParams(mailToUrl[1]), params = new URLSearchParams(mailToUrl[1]),
toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null; to = params.get('to'),
toEmailModel = value => EmailCollectionModel.fromString(value);
showMessageComposer([ showMessageComposer([
ComposeType.Empty, ComposeType.Empty,
null, null,
params.get('to') toEmailModel(to ? email + ',' + to : email),
? Object.values(
toEmailModel(email + ',' + params.get('to')).reduce((result, value) => {
if (value) {
if (result[value.email]) {
if (!result[value.email].name) {
result[value.email] = value;
}
} else {
result[value.email] = value;
}
}
return result;
}, {})
)
: EmailModel.parseEmailLine(email),
toEmailModel(params.get('cc')), toEmailModel(params.get('cc')),
toEmailModel(params.get('bcc')), toEmailModel(params.get('bcc')),
params.get('subject'), params.get('subject'),
@ -272,49 +258,38 @@ populateMessageBody = (oMessage, popup) => {
} }
} else { } else {
let json = oData?.Result; let json = oData?.Result;
if (json
if ( && MessageModel.validJson(json)
json && && oMessage.hash === json.hash
MessageModel.validJson(json) && // && oMessage.folder === json.folder
oMessage.folder === json.folder // && oMessage.uid == json.uid
&& oMessage.revivePropertiesFromJson(json)
) { ) {
const threads = oMessage.threads(),
isNew = !popup && oMessage.uid != json.uid && threads.includes(json.uid),
messagesDom = MessageUserStore.bodiesDom();
if (isNew) {
oMessage = MessageModel.reviveFromJson(json);
if (oMessage) {
oMessage.threads(threads);
MessageFlagsCache.initMessage(oMessage);
// Set clone
oMessage = MessageModel.fromMessageListItem(oMessage);
}
MessageUserStore.message(oMessage);
}
if (oMessage && oMessage.uid == json.uid) {
popup || MessageUserStore.error('');
/* /*
if (bCached) { if (bCached) {
delete json.flags; delete json.flags;
} }
*/ */
isNew || oMessage.revivePropertiesFromJson(json); if (popup) {
oMessage.viewPopupMessage();
} else {
MessageUserStore.error('');
const messagesDom = MessageUserStore.bodiesDom();
if (messagesDom) { if (messagesDom) {
let id = 'rl-msg-' + oMessage.hash.replace(/[^a-zA-Z0-9]/g, ''), let id = 'rl-msg-' + oMessage.hash,
body = elementById(id); body = elementById(id);
if (body) { if (body) {
oMessage.body = body; oMessage.body = body;
oMessage.isHtml(body.classList.contains('html')); oMessage.isHtml(body.classList.contains('html'));
oMessage.hasImages(body.rlHasImages); oMessage.hasImages(body.rlHasImages);
} else { } else {
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part ' body = createElement('div',{
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '') id:id,
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '') hidden:'',
+ '">' class:'b-text-part'
+ '</div>'); + (oMessage.pgpSigned() ? ' openpgp-signed' : '')
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
});
oMessage.body = body; oMessage.body = body;
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) { if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
oMessage.viewPlain(); oMessage.viewPlain();
@ -325,42 +300,16 @@ populateMessageBody = (oMessage, popup) => {
messagesDom.append(body); messagesDom.append(body);
popup || (oMessage.body.hidden = false); oMessage.body.hidden = false;
popup && oMessage.viewPopupMessage();
} }
}
MessageFlagsCache.initMessage(oMessage); MessageFlagsCache.initMessage(oMessage);
if (oMessage.isUnseen()) { if (oMessage.isUnseen()) {
MessageUserStore.MessageSeenTimer = setTimeout( MessageUserStore.MessageSeenTimer = setTimeout(
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]), () => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
SettingsUserStore.messageReadDelay() * 1000 // seconds SettingsUserStore.messageReadDelay() * 1000 // seconds
); );
}
if (isNew) {
let selectedMessage = MessagelistUserStore.selectedMessage();
if (
selectedMessage &&
(oMessage.folder !== selectedMessage.folder || oMessage.uid != selectedMessage.uid)
) {
MessagelistUserStore.selectedMessage(null);
if (1 === MessagelistUserStore.length) {
MessagelistUserStore.focusedMessage(null);
}
} else if (!selectedMessage) {
selectedMessage = MessagelistUserStore.find(
subMessage =>
subMessage &&
subMessage.folder === oMessage.folder &&
subMessage.uid == oMessage.uid
);
if (selectedMessage) {
MessagelistUserStore.selectedMessage(selectedMessage);
MessagelistUserStore.focusedMessage(selectedMessage);
}
}
}
} }
} }
} }

View file

@ -1,8 +1,29 @@
import { doc, createElement, addEventsListeners } from 'Common/Globals'; import { doc, createElement, addEventsListeners } from 'Common/Globals';
import { EmailModel } from 'Model/Email'; import { EmailModel, addressparser } from 'Model/Email';
const contentType = 'snappymail/emailaddress', const contentType = 'snappymail/emailaddress',
getAddressKey = li => li?.emailaddress?.key; getAddressKey = li => li?.emailaddress?.key,
parseEmailLine = line => addressparser(line).map(item =>
(item.name || item.email)
? new EmailModel(item.email, item.name) : null
).filter(v => v),
splitEmailLine = line => {
const result = [];
let exists = false;
addressparser(line).forEach(item => {
const address = (item.name || item.email)
? new EmailModel(item.email, item.name)
: null;
if (address?.email) {
exists = true;
}
result.push(address ? address.toLine() : item.name);
});
return exists ? result : null;
};
let dragAddress, datalist; let dragAddress, datalist;
@ -154,8 +175,8 @@ export class EmailAddressesComponent {
if (val) { if (val) {
const self = this, const self = this,
v = val.trim(), v = val.trim(),
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? EmailModel.splitEmailLine(val) : null, hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? splitEmailLine(val) : null,
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value)) values = (hook || [val]).map(value => parseEmailLine(value))
.flat(Infinity) .flat(Infinity)
.map(item => (item.toLine ? [item.toLine(), item] : [item, null])); .map(item => (item.toLine ? [item.toLine(), item] : [item, null]));

View file

@ -59,6 +59,7 @@ export class AbstractViewPopup extends AbstractView
super('Popups' + name, ViewTypePopup); super('Popups' + name, ViewTypePopup);
this.keyScope.scope = name; this.keyScope.scope = name;
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 }); this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
this.close = () => this.modalVisible(false);
addShortcut('escape,close', '', name, () => { addShortcut('escape,close', '', name, () => {
if (this.modalVisible() && false !== this.onClose()) { if (this.modalVisible() && false !== this.onClose()) {
this.close(); this.close();
@ -78,8 +79,6 @@ export class AbstractViewPopup extends AbstractView
afterShow() {} // Happens after showModal() animation transitionend afterShow() {} // Happens after showModal() animation transitionend
onHide() {} // Happens before animation transitionend onHide() {} // Happens before animation transitionend
afterHide() {} // Happens after animation transitionend afterHide() {} // Happens after animation transitionend
close() {}
*/ */
} }

View file

@ -1,6 +1,6 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko'; import { koComputable } from 'External/ko';
import { doc, $htmlCL, elementById, fireEvent } from 'Common/Globals'; import { doc, $htmlCL, elementById, createElement, fireEvent } from 'Common/Globals';
import { forEachObjectEntry } from 'Common/Utils'; import { forEachObjectEntry } from 'Common/Utils';
import { i18nToNodes } from 'Common/Translator'; import { i18nToNodes } from 'Common/Translator';
@ -40,22 +40,20 @@ const
ViewModelClass.__vm = vm; ViewModelClass.__vm = vm;
if (vmPlace) { if (vmPlace) {
vmDom = Element.fromHTML(dialog vmDom = dialog
? '<dialog id="V-'+ id + '"></dialog>' ? createElement('dialog',{id:'V-'+id})
: '<div id="V-'+ id + '" hidden=""></div>'); : createElement('div',{id:'V-'+id,hidden:''})
vmPlace.append(vmDom); vmPlace.append(vmDom);
vm.viewModelDom = ViewModelClass.__dom = vmDom; vm.viewModelDom = ViewModelClass.__dom = vmDom;
if (dialog) { if (dialog) {
vm.close = () => hideScreenPopup(ViewModelClass);
// Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined // Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined
if (!vmDom.showModal) { if (!vmDom.showModal) {
vmDom.className = 'polyfill'; vmDom.className = 'polyfill';
vmDom.showModal = () => { vmDom.showModal = () => {
vmDom.backdrop || vmDom.backdrop ||
vmDom.before(vmDom.backdrop = Element.fromHTML('<div class="dialog-backdrop"></div>')); vmDom.before(vmDom.backdrop = createElement('div',{class:'dialog-backdrop'}));
vmDom.setAttribute('open',''); vmDom.setAttribute('open','');
vmDom.open = true; vmDom.open = true;
vmDom.returnValue = null; vmDom.returnValue = null;
@ -155,81 +153,64 @@ const
}); });
}, },
/**
* @param {Function} ViewModelClassToHide
* @returns {void}
*/
hideScreenPopup = ViewModelClassToHide =>
ViewModelClassToHide?.__vm && ViewModelClassToHide.__dom
&& ViewModelClassToHide.__vm.modalVisible(false),
/** /**
* @param {string} screenName * @param {string} screenName
* @param {string} subPart * @param {string} subPart
* @returns {void} * @returns {void}
*/ */
screenOnRoute = (screenName, subPart) => { screenOnRoute = (screenName, subPart) => {
let vmScreen = null, screenName = screenName || defaultScreenName;
isSameScreen = false; if (screenName && fireEvent('sm-show-screen', screenName, 1)) {
if (null == screenName || '' == screenName) {
screenName = defaultScreenName;
}
if (fireEvent('sm-show-screen', screenName, 1)) {
// Close all popups // Close all popups
for (let vm of visiblePopups) { for (let vm of visiblePopups) {
(false === vm.onClose()) || vm.close(); (false === vm.onClose()) || vm.close();
} }
if (screenName) { let vmScreen = screen(screenName);
vmScreen = screen(screenName); if (!vmScreen) {
if (!vmScreen) { vmScreen = screen(defaultScreenName);
vmScreen = screen(defaultScreenName); if (vmScreen) {
if (vmScreen) { subPart = screenName + '/' + subPart;
subPart = screenName + '/' + subPart; screenName = defaultScreenName;
screenName = defaultScreenName; }
} }
if (vmScreen?.__started) {
let isSameScreen = currentScreen && vmScreen === currentScreen;
if (!vmScreen.__builded) {
vmScreen.__builded = true;
vmScreen.viewModels.forEach(ViewModelClass =>
buildViewModel(ViewModelClass, vmScreen)
);
vmScreen.onBuild?.();
} }
if (vmScreen?.__started) { setTimeout(() => {
isSameScreen = currentScreen && vmScreen === currentScreen; // hide screen
currentScreen && !isSameScreen && hideScreen(currentScreen);
// --
if (!vmScreen.__builded) { currentScreen = vmScreen;
vmScreen.__builded = true;
vmScreen.viewModels.forEach(ViewModelClass => // show screen
buildViewModel(ViewModelClass, vmScreen) if (!isSameScreen) {
); vmScreen.onShow?.();
vmScreen.onBuild?.(); forEachViewModel(vmScreen, (vm, dom) => {
vm.beforeShow?.();
i18nToNodes(dom);
dom.hidden = false;
vm.onShow?.();
autofocus(dom);
});
} }
// --
setTimeout(() => { vmScreen.__cross?.parse(subPart);
// hide screen }, 1);
currentScreen && !isSameScreen && hideScreen(currentScreen);
// --
currentScreen = vmScreen;
// show screen
if (!isSameScreen) {
vmScreen.onShow?.();
forEachViewModel(vmScreen, (vm, dom) => {
vm.beforeShow?.();
i18nToNodes(dom);
dom.hidden = false;
vm.onShow?.();
autofocus(dom);
});
}
// --
vmScreen.__cross?.parse(subPart);
}, 1);
}
} }
} }
}; };
@ -285,7 +266,7 @@ export const
}); });
const cross = new Crossroads(); const cross = new Crossroads();
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute); cross.addRoute(/^([^/]*)\/?(.*)$/, screenOnRoute);
hasher.add(cross.parse.bind(cross)); hasher.add(cross.parse.bind(cross));
hasher.init(); hasher.init();

View file

@ -1,7 +1,6 @@
import { ParseMime } from 'Mime/Parser'; import { ParseMime } from 'Mime/Parser';
import { AttachmentModel } from 'Model/Attachment'; import { AttachmentModel } from 'Model/Attachment';
import { EmailModel } from 'Model/Email';
import { FileInfo } from 'Common/File'; import { FileInfo } from 'Common/File';
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp'; import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
@ -17,16 +16,10 @@ export function MimeToMessage(data, message)
let html = struct.getByContentType('text/html'); let html = struct.getByContentType('text/html');
html = html ? html.body : ''; html = html ? html.body : '';
if (struct.headers.subject) { message.subject(struct.headerValue('subject') || '');
message.subject(struct.headers.subject.value);
} // EmailCollectionModel
['from','to'].forEach(name => { ['from','to'].forEach(name => message[name].fromString(struct.headerValue(name)));
if (struct.headers[name] && !message[name].length) {
let mail = new EmailModel;
mail.parse(struct.headers[name].value);
message[name].push(mail);
}
});
struct.forEach(part => { struct.forEach(part => {
let cd = part.header('content-disposition'), let cd = part.header('content-disposition'),
@ -40,7 +33,7 @@ export function MimeToMessage(data, message)
attachment.fileNameExt = attachment.fileName.replace(/^.+(\.[a-z]+)$/, '$1'); attachment.fileNameExt = attachment.fileName.replace(/^.+(\.[a-z]+)$/, '$1');
attachment.fileType = FileInfo.getType('', type.value); attachment.fileType = FileInfo.getType('', type.value);
attachment.url = part.dataUrl; attachment.url = part.dataUrl;
attachment.friendlySize = FileInfo.friendlySize(part.body.length); attachment.estimatedSize = part.body.length;
/* /*
attachment.isThumbnail = false; attachment.isThumbnail = false;
attachment.contentLocation = ''; attachment.contentLocation = '';

View file

@ -18,32 +18,83 @@ import { AbstractModel } from 'Knoin/AbstractModel';
* @param {String} str Address field * @param {String} str Address field
* @return {Array} An array of address objects * @return {Array} An array of address objects
*/ */
function addressparser(str) { export function addressparser(str) {
var tokenizer = new Tokenizer(str); str = (str || '').toString();
var tokens = tokenizer.tokenize();
var addresses = [];
var address = [];
var parsedAddresses = [];
tokens.forEach(token => { let
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) { endOperator = '',
address.length && addresses.push(address); node = {
type: 'text',
value: ''
},
escaped = false,
address = [],
addresses = [];
const
/*
* Operator tokens and which tokens are expected to end the sequence
*/
OPERATORS = {
'"': '"',
'(': ')',
'<': '>',
',': '',
// Groups are ended by semicolons
':': ';',
// Semicolons are not a legal delimiter per the RFC2822 grammar other
// than for terminating a group, but they are also not valid for any
// other use in this context. Given that some mail clients have
// historically allowed the semicolon as a delimiter equivalent to the
// comma in their UI, it makes sense to treat them the same as a comma
// when used outside of a group.
';': ''
},
pushToken = token => {
token.value = (token.value || '').toString().trim();
token.value.length && address.push(token);
node = {
type: 'text',
value: ''
},
escaped = false;
},
pushAddress = () => {
if (address.length) {
address = _handleAddress(address);
if (address.length) {
addresses = addresses.concat(address);
}
}
address = []; address = [];
};
[...str].forEach(chr => {
if (!escaped && (chr === endOperator || (!endOperator && chr in OPERATORS))) {
pushToken(node);
if (',' === chr || ';' === chr) {
pushAddress();
} else {
endOperator = endOperator ? '' : OPERATORS[chr];
if ('<' === chr) {
node.type = 'email';
} else if ('(' === chr) {
node.type = 'comment';
} else if (':' === chr) {
node.type = 'group';
}
}
} else { } else {
address.push(token); node.value += chr;
escaped = !escaped && '\\' === chr;
} }
}); });
pushToken(node);
address.length && addresses.push(address); pushAddress();
addresses.forEach(address => { return addresses;
address = _handleAddress(address); // return addresses.map(item => (item.name || item.email) ? new EmailModel(item.email, item.name) : null).filter(v => v);
if (address.length) {
parsedAddresses = parsedAddresses.concat(address);
}
});
return parsedAddresses;
} }
/** /**
@ -53,37 +104,20 @@ function addressparser(str) {
* @return {Object} Address object * @return {Object} Address object
*/ */
function _handleAddress(tokens) { function _handleAddress(tokens) {
var isGroup = false; let
var state = 'text'; isGroup = false,
var address = void 0; address = {},
var addresses = []; addresses = [],
var data = { data = {
address: [], email: [],
comment: [], comment: [],
group: [], group: [],
text: [] text: []
}; };
// Filter out <addresses>, (comments) and regular text
tokens.forEach(token => { tokens.forEach(token => {
if (token.type === 'operator') { isGroup = isGroup || 'group' === token.type;
switch (token.value) { data[token.type].push(token.value);
case '<':
state = 'address';
break;
case '(':
state = 'comment';
break;
case ':':
state = 'group';
isGroup = true;
break;
default:
state = 'text';
}
} else if (token.value) {
data[state].push(token.value);
}
}); });
// If there is no text but a comment, replace the two // If there is no text but a comment, replace the two
@ -94,179 +128,90 @@ function _handleAddress(tokens) {
if (isGroup) { if (isGroup) {
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3 // http://tools.ietf.org/html/rfc2822#appendix-A.1.3
data.text = data.text.join(' '); /*
addresses.push({ addresses.push({
name: data.text || address && address.name, email: '',
group: data.group.length ? addressparser(data.group.join(',')) : [] name: data.text.join(' ').trim(),
group: addressparser(data.group.join(','))
// ,comment: data.comment.join(' ').trim()
}); });
*/
addresses = addresses.concat(addressparser(data.group.join(',')));
} else { } else {
// If no address was found, try to detect one from regular text // If no address was found, try to detect one from regular text
if (!data.address.length && data.text.length) { if (!data.email.length && data.text.length) {
var i = data.text.length; var i = data.text.length;
while (i--) { while (i--) {
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) { if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
data.address = data.text.splice(i, 1); data.email = data.text.splice(i, 1);
break; break;
} }
} }
// still no address // still no address
if (!data.address.length) { if (!data.email.length) {
i = data.text.length; i = data.text.length;
while (i--) { while (i--) {
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, address => { data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, address => {
if (!data.address.length) { if (!data.email.length) {
data.address = [address.trim()]; data.email = [address.trim()];
return ''; return '';
} }
return address.trim(); return address.trim();
}); });
if (data.address.length) { if (data.email.length) {
break; break;
} }
} }
} }
} }
// If there's still is no text but a comment exixts, replace the two // If there's still no text but a comment exists, replace the two
if (!data.text.length && data.comment.length) { if (!data.text.length && data.comment.length) {
data.text = data.comment; data.text = data.comment;
data.comment = []; data.comment = [];
} }
// Keep only the first address occurence, push others to regular text // Keep only the first address occurence, push others to regular text
if (data.address.length > 1) { if (data.email.length > 1) {
data.text = data.text.concat(data.address.splice(1)); data.text = data.text.concat(data.email.splice(1));
} }
// Join values with spaces
data.text = data.text.join(' ');
data.address = data.address.join(' ');
if (!data.address && isGroup) {
return [];
}
address = { address = {
address: data.address || data.text || '', // Join values with spaces
name: data.text || data.address || '' email: data.email.join(' ').trim(),
name: data.text.join(' ').trim()
// ,comment: data.comment.join(' ').trim()
}; };
if (address.address === address.name) { if (address.email === address.name) {
if ((address.address || '').match(/@/)) { if (address.email.includes('@')) {
address.name = ''; address.name = '';
} else { } else {
address.address = ''; address.email = '';
} }
} }
// address.email = address.email.replace(/^[<]+(.*)[>]+$/g, '$1');
addresses.push(address); addresses.push(address);
} }
return addresses; return addresses;
} }
/*
* Operator tokens and which tokens are expected to end the sequence
*/
var OPERATORS = {
'"': '"',
'(': ')',
'<': '>',
',': '',
// Groups are ended by semicolons
':': ';',
// Semicolons are not a legal delimiter per the RFC2822 grammar other
// than for terminating a group, but they are also not valid for any
// other use in this context. Given that some mail clients have
// historically allowed the semicolon as a delimiter equivalent to the
// comma in their UI, it makes sense to treat them the same as a comma
// when used outside of a group.
';': ''
};
class Tokenizer
{
constructor(str) {
this.str = (str || '').toString();
this.operatorCurrent = '';
this.operatorExpecting = '';
this.node = null;
this.escaped = false;
this.list = [];
}
tokenize() {
var list = [];
[...this.str].forEach(c => this.checkChar(c));
this.list.forEach(node => {
node.value = (node.value || '').toString().trim();
node.value && list.push(node);
});
return list;
}
checkChar(chr) {
if ((chr in OPERATORS || chr === '\\') && this.escaped) {
this.escaped = false;
} else if (this.operatorExpecting && chr === this.operatorExpecting) {
this.node = {
type: 'operator',
value: chr
};
this.list.push(this.node);
this.node = null;
this.operatorExpecting = '';
this.escaped = false;
return;
} else if (!this.operatorExpecting && chr in OPERATORS) {
this.node = {
type: 'operator',
value: chr
};
this.list.push(this.node);
this.node = null;
this.operatorExpecting = OPERATORS[chr];
this.escaped = false;
return;
}
if (!this.escaped && chr === '\\') {
this.escaped = true;
return;
}
if (!this.node) {
this.node = {
type: 'text',
value: ''
};
this.list.push(this.node);
}
if (this.escaped && chr !== '\\') {
this.node.value += '\\';
}
this.node.value += chr;
this.escaped = false;
}
}
export class EmailModel extends AbstractModel { export class EmailModel extends AbstractModel {
/** /**
* @param {string=} email = '' * @param {string=} email = ''
* @param {string=} name = '' * @param {string=} name = ''
* @param {string=} dkimStatus = 'none' * @param {string=} dkimStatus = 'none'
*/ */
constructor(email = '', name = '', dkimStatus = 'none') { constructor(email, name, dkimStatus = 'none') {
super(); super();
this.email = email; this.email = email || '';
this.name = name; this.name = name || '';
this.dkimStatus = dkimStatus; this.dkimStatus = dkimStatus;
this.cleanup();
this.clearDuplicateName();
} }
/** /**
@ -276,60 +221,34 @@ export class EmailModel extends AbstractModel {
*/ */
static reviveFromJson(json) { static reviveFromJson(json) {
const email = super.reviveFromJson(json); const email = super.reviveFromJson(json);
email?.clearDuplicateName(); email?.cleanup();
return email; return email?.valid() ? email : null;
}
/**
* @returns {void}
*/
clear() {
this.email = '';
this.name = '';
this.dkimStatus = 'none';
} }
/** /**
* @returns {boolean} * @returns {boolean}
*/ */
validate() { valid() {
return this.name || this.email; return this.name || this.email;
} }
/**
* @param {boolean} withoutName = false
* @returns {string}
*/
hash(withoutName = false) {
return '#' + (withoutName ? '' : this.name) + '#' + this.email + '#';
}
/** /**
* @returns {void} * @returns {void}
*/ */
clearDuplicateName() { cleanup() {
if (this.name === this.email) { if (this.name === this.email) {
this.name = ''; this.name = '';
} }
} }
/**
* @param {string} query
* @returns {boolean}
*/
search(query) {
return (this.name + ' ' + this.email).toLowerCase().includes(query.toLowerCase());
}
/** /**
* @param {boolean} friendlyView = false * @param {boolean} friendlyView = false
* @param {boolean} wrapWithLink = false * @param {boolean} wrapWithLink = false
* @returns {string} * @returns {string}
*/ */
toLine(friendlyView, wrapWithLink) { toLine(friendlyView, wrapWithLink) {
let result = this.email, let name = this.name,
name = this.name, result = this.email,
toLink = text => toLink = text =>
'<a href="mailto:' '<a href="mailto:'
+ encodeHtml(result) + (name ? '?to=' + encodeURIComponent('"' + name + '" <' + result + '>') : '') + encodeHtml(result) + (name ? '?to=' + encodeURIComponent('"' + name + '" <' + result + '>') : '')
@ -348,47 +267,6 @@ export class EmailModel extends AbstractModel {
result = toLink(); result = toLink();
} }
} }
return result; return result || name;
}
static splitEmailLine(line) {
const result = [];
let exists = false;
addressparser(line).forEach(item => {
const address = item.address
? new EmailModel(item.address.replace(/^[<]+(.*)[>]+$/g, '$1'), item.name || '')
: null;
if (address?.email) {
exists = true;
}
result.push(address ? address.toLine() : item.name);
});
return exists ? result : null;
}
static parseEmailLine(line) {
return addressparser(line).map(item =>
item.address ? new EmailModel(item.address.replace(/^[<]+(.*)[>]+$/g, '$1'), item.name || '') : null
).filter(v => v);
}
/**
* @param {string} emailAddress
* @returns {boolean}
*/
parse(emailAddress) {
emailAddress = emailAddress.trim();
if (emailAddress) {
const result = addressparser(emailAddress);
if (result.length) {
this.name = result[0].name || '';
this.email = result[0].address || '';
this.clearDuplicateName();
return true;
}
}
return false;
} }
} }

View file

@ -1,5 +1,6 @@
import { AbstractCollectionModel } from 'Model/AbstractCollection'; import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { EmailModel } from 'Model/Email'; import { EmailModel, addressparser } from 'Model/Email';
import { forEachObjectValue } from 'Common/Utils';
'use strict'; 'use strict';
@ -13,6 +14,16 @@ export class EmailCollectionModel extends AbstractCollectionModel
return super.reviveFromJson(items, email => EmailModel.reviveFromJson(email)); return super.reviveFromJson(items, email => EmailModel.reviveFromJson(email));
} }
/**
* @param {string} text
* @returns {EmailCollectionModel}
*/
static fromString(str) {
let list = new this();
list.fromString(str);
return list;
}
/** /**
* @param {boolean=} friendlyView = false * @param {boolean=} friendlyView = false
* @param {boolean=} wrapWithLink = false * @param {boolean=} wrapWithLink = false
@ -21,4 +32,23 @@ export class EmailCollectionModel extends AbstractCollectionModel
toString(friendlyView, wrapWithLink) { toString(friendlyView, wrapWithLink) {
return this.map(email => email.toLine(friendlyView, wrapWithLink)).join(', '); return this.map(email => email.toLine(friendlyView, wrapWithLink)).join(', ');
} }
/**
* @param {string} text
*/
fromString(str) {
if (str) {
let items = {}, key;
addressparser(str).forEach(item => {
item = new EmailModel(item.email, item.name);
// Make them unique
key = item.email || item.name;
if (key && (item.name || !items[key])) {
items[key] = item;
}
});
forEachObjectValue(items, item => this.push(item));
}
}
} }

View file

@ -5,7 +5,7 @@ import { i18n } from 'Common/Translator';
import { doc, SettingsGet } from 'Common/Globals'; import { doc, SettingsGet } from 'Common/Globals';
import { encodeHtml, plainToHtml, htmlToPlain, cleanHtml } from 'Common/Html'; import { encodeHtml, plainToHtml, htmlToPlain, cleanHtml } from 'Common/Html';
import { isFunction, forEachObjectEntry } from 'Common/Utils'; import { forEachObjectEntry } from 'Common/Utils';
import { serverRequestRaw, proxy } from 'Common/Links'; import { serverRequestRaw, proxy } from 'Common/Links';
import { addObservablesTo, addComputablesTo } from 'External/ko'; import { addObservablesTo, addComputablesTo } from 'External/ko';
@ -25,6 +25,8 @@ import { LanguageStore } from 'Stores/Language';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
const const
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
toggleTag = (message, keyword) => { toggleTag = (message, keyword) => {
const lower = keyword.toLowerCase(), const lower = keyword.toLowerCase(),
flags = message.flags, flags = message.flags,
@ -212,6 +214,7 @@ export class MessageModel extends AbstractModel {
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.attachments, this.foundCIDs)); // this.attachments(AttachmentCollectionModel.reviveFromJson(json.attachments, this.foundCIDs));
this.computeSenderEmail(); this.computeSenderEmail();
return true;
} }
} }
@ -290,7 +293,7 @@ export class MessageModel extends AbstractModel {
viewHtml() { viewHtml() {
const body = this.body; const body = this.body;
if (body && this.html()) { if (body && this.html()) {
let result = cleanHtml(this.html(), this.attachments()); let result = msgHtml(this);
this.hasExternals(result.hasExternals); this.hasExternals(result.hasExternals);
this.hasImages(body.rlHasImages = !!result.hasExternals); this.hasImages(body.rlHasImages = !!result.hasExternals);
@ -300,9 +303,7 @@ export class MessageModel extends AbstractModel {
body.classList.toggle('plain', 0); body.classList.toggle('plain', 0);
if (!this.isSpam() && FolderUserStore.spamFolder() != this.folder) { if (!this.isSpam() && FolderUserStore.spamFolder() != this.folder) {
if (('dkim' === SettingsUserStore.viewImages() && 'pass' === this.dkim[0]?.[0]) if ('always' === SettingsUserStore.viewImages()) {
|| 'always' === SettingsUserStore.viewImages()
) {
this.showExternalImages(); this.showExternalImages();
} }
if ('match' === SettingsUserStore.viewImages()) { if ('match' === SettingsUserStore.viewImages()) {
@ -335,26 +336,30 @@ export class MessageModel extends AbstractModel {
} }
viewPopupMessage(print) { viewPopupMessage(print) {
const timeStampInUTC = this.dateTimeStampInUTC() || 0, const
timeStampInUTC = this.dateTimeStampInUTC() || 0,
ccLine = this.cc.toString(), ccLine = this.cc.toString(),
bccLine = this.bcc.toString(),
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null, m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
win = open(''), win = open('', 'sm-msg-'+this.requestHash
sdoc = win.document; /*,newWindow ? 'innerWidth=' + elementById('V-MailMessageView').clientWidth : ''*/
let subject = encodeHtml(this.subject()), ),
sdoc = win.document,
subject = encodeHtml(this.subject()),
mode = this.isHtml() ? 'div' : 'pre', mode = this.isHtml() ? 'div' : 'pre',
cc = ccLine ? `<div>${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}</div>` : '', to = `<div>${encodeHtml(i18n('GLOBAL/TO'))}: ${encodeHtml(this.to)}</div>`
+ (ccLine ? `<div>${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}</div>` : '')
+ (bccLine ? `<div>${encodeHtml(i18n('GLOBAL/BCC'))}: ${encodeHtml(bccLine)}</div>` : ''),
style = getComputedStyle(doc.querySelector('.messageView')), style = getComputedStyle(doc.querySelector('.messageView')),
prop = property => style.getPropertyValue(property); prop = property => style.getPropertyValue(property);
sdoc.write(PreviewHTML sdoc.write(PreviewHTML
.replace('<title>', '<title>'+subject) .replace('<title>', '<title>'+subject)
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
.replace('<body>', `<body style="background-color:${prop('background-color')};color:${prop('color')}"><header><h1>${subject}</h1><time>${encodeHtml(m ? m.format('LLL',0,LanguageStore.hourCycle()) : '')}</time><div>${encodeHtml(this.from)}</div><div>${encodeHtml(i18n('GLOBAL/TO'))}: ${encodeHtml(this.to)}</div>${cc}</header><${mode}>${this.bodyAsHTML()}</${mode}>`) .replace('<body>', `<body style="background-color:${prop('background-color')};color:${prop('color')}"><header><h1>${subject}</h1><time>${encodeHtml(m ? m.format('LLL',0,LanguageStore.hourCycle()) : '')}</time><div>${encodeHtml(this.from)}</div>${to}</header><${mode}>${this.bodyAsHTML()}</${mode}>`)
); );
sdoc.close(); sdoc.close();
if (print) { print && setTimeout(() => win.print(), 100);
setTimeout(() => win.print(), 100);
}
} }
/** /**
@ -376,34 +381,39 @@ export class MessageModel extends AbstractModel {
} }
/** /**
* @param {MessageModel} message
* @returns {MessageModel} * @returns {MessageModel}
*/ *//*
static fromMessageListItem(message) { clone() {
let self = new MessageModel(); let self = new MessageModel();
if (message) { // Clone message values
// Clone message values forEachObjectEntry(this, (key, value) => {
forEachObjectEntry(message, (key, value) => { if (ko.isObservable(value)) {
if (ko.isObservable(value)) { ko.isComputed(value) || self[key](value());
ko.isComputed(value) || self[key](value()); } else if (!isFunction(value)) {
} else if (!isFunction(value)) { self[key] = value;
self[key] = value; }
} });
}); self.computeSenderEmail();
self.computeSenderEmail();
}
return self; return self;
} }*/
showExternalImages(regex) { showExternalImages(regex) {
const body = this.body; const body = this.body;
if (body && this.hasImages()) { if (body && this.hasImages()) {
if (regex) { if (regex) {
regex = SettingsUserStore.viewImagesWhitelist() regex = [];
.trim() SettingsUserStore.viewImagesWhitelist().trim().split(/[\s\r\n,;]+/g).forEach(rule => {
.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&') rule = rule.split('+');
.replace(/[\s\r\n,;]+/g, '|') rule[0] = rule[0].trim();
.replace(/\|+/g, '|'); if (rule[0]
&& (!rule.includes('spf') || 'pass' === this.spf[0]?.[0])
&& (!rule.includes('dkim') || 'pass' === this.dkim[0]?.[0])
&& (!rule.includes('dmarc') || 'pass' === this.dmarc[0]?.[0])
) {
regex.push(rule[0].replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'));
}
});
regex = regex.join('|').replace(/\|+/g, '|');
if (regex) { if (regex) {
console.log('whitelist images = '+regex); console.log('whitelist images = '+regex);
regex = new RegExp(regex); regex = new RegExp(regex);
@ -452,7 +462,7 @@ export class MessageModel extends AbstractModel {
); );
return clone.innerHTML; return clone.innerHTML;
} }
let result = cleanHtml(this.html(), this.attachments()) let result = msgHtml(this);
return result.html || plainToHtml(this.plain()); return result.html || plainToHtml(this.plain());
} }

View file

@ -1,5 +1,6 @@
import { AbstractCollectionModel } from 'Model/AbstractCollection'; import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { MessageModel } from 'Model/Message'; import { MessageModel } from 'Model/Message';
import { MessageUserStore } from 'Stores/User/Message';
import { import {
MessageFlagsCache MessageFlagsCache
@ -31,8 +32,11 @@ export class MessageCollectionModel extends AbstractCollectionModel
* @returns {MessageCollectionModel} * @returns {MessageCollectionModel}
*/ */
static reviveFromJson(object, cached) { static reviveFromJson(object, cached) {
let msg = MessageUserStore.message();
return super.reviveFromJson(object, message => { return super.reviveFromJson(object, message => {
message = MessageModel.reviveFromJson(message); // If message is currently viewed, use that.
// Maybe then use msg.revivePropertiesFromJson(message) ?
message = (msg && msg.hash === message.hash) ? msg : MessageModel.reviveFromJson(message);
if (message) { if (message) {
message.deleted(false); message.deleted(false);
cached ? MessageFlagsCache.initMessage(message) : MessageFlagsCache.store(message); cached ? MessageFlagsCache.initMessage(message) : MessageFlagsCache.store(message);

View file

@ -44,7 +44,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
['layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval', ['layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles',
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes', 'maxBlockquotesLevel', 'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes', 'maxBlockquotesLevel',
'useCheckboxesInList', 'listGrouped', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck' 'useCheckboxesInList', 'listGrouped', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck'
].forEach(name => this[name] = SettingsUserStore[name]); ].forEach(name => this[name] = SettingsUserStore[name]);
@ -102,7 +102,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.addSetting('Layout'); this.addSetting('Layout');
this.addSetting('MaxBlockquotesLevel'); this.addSetting('MaxBlockquotesLevel');
this.addSettings(['ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'HideDeleted', this.addSettings(['ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'HideDeleted', 'AllowStyles',
'ListInlineAttachments', 'simpleAttachmentsList', 'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder', 'ListInlineAttachments', 'simpleAttachmentsList', 'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder',
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'allowSpellcheck', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'allowSpellcheck',
'DesktopNotifications', 'SoundNotification', 'CollapseBlockquotes']); 'DesktopNotifications', 'SoundNotification', 'CollapseBlockquotes']);

View file

@ -19,6 +19,7 @@ export const SettingsUserStore = new class {
viewImages: 0, viewImages: 0,
viewImagesWhitelist: '', viewImagesWhitelist: '',
removeColors: 0, removeColors: 0,
allowStyles: 0,
collapseBlockquotes: 1, collapseBlockquotes: 1,
maxBlockquotesLevel: 0, maxBlockquotesLevel: 0,
listInlineAttachments: 0, listInlineAttachments: 0,
@ -88,6 +89,7 @@ export const SettingsUserStore = new class {
self.viewImages(SettingsGet('ViewImages')); self.viewImages(SettingsGet('ViewImages'));
self.viewImagesWhitelist(SettingsGet('ViewImagesWhitelist')); self.viewImagesWhitelist(SettingsGet('ViewImagesWhitelist'));
self.removeColors(SettingsGet('RemoveColors')); self.removeColors(SettingsGet('RemoveColors'));
self.allowStyles(SettingsGet('AllowStyles'));
self.collapseBlockquotes(SettingsGet('CollapseBlockquotes')); self.collapseBlockquotes(SettingsGet('CollapseBlockquotes'));
self.maxBlockquotesLevel(SettingsGet('MaxBlockquotesLevel')); self.maxBlockquotesLevel(SettingsGet('MaxBlockquotesLevel'));
self.listInlineAttachments(SettingsGet('ListInlineAttachments')); self.listInlineAttachments(SettingsGet('ListInlineAttachments'));

View file

@ -62,9 +62,14 @@
#rl-left { #rl-left {
display: none; display: none;
} }
#rl-content, #rl-right {
display: block;
width: 100%;
}
.message-selected { .message-selected {
.close, .close,
.infoParent, .infoParent,
.top-toolbar,
.messageView .controls-handle, .messageView .controls-handle,
#tags-dropdown-id, #tags-dropdown-id,
.message-fixed-button-toolbar, .message-fixed-button-toolbar,
@ -75,6 +80,7 @@
} }
#V-MailMessageView { #V-MailMessageView {
left: 0; left: 0;
overflow: initial;
position: initial; position: initial;
} }
.messageView, .messageView,

View file

@ -22,7 +22,7 @@ option:disabled {
-webkit-tap-highlight-color: rgba(0,0,0,0); -webkit-tap-highlight-color: rgba(0,0,0,0);
} }
#rl-content { #rl-content:not([hidden]) {
display:flex; display:flex;
height: 100%; height: 100%;
} }

View file

@ -14,7 +14,7 @@
z-index: 0; z-index: 0;
} }
#rl-right { #rl-right:not([hidden]) {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
width: 20%; width: 20%;
@ -205,7 +205,7 @@ html.rl-mobile {
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 50vw; width: 50vw;
z-index: 2; z-index: 3;
} }
&.rl-left-panel-disabled #rl-left { &.rl-left-panel-disabled #rl-left {
display: none; display: none;

View file

@ -348,13 +348,13 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
.sm-bq-switcher > summary { .sm-bq-switcher > summary {
border: 1px solid #999; border: 1px solid #999;
display: block;
width: 3em;
line-height: 1em;
text-align: center;
cursor: pointer; cursor: pointer;
line-height: 1em;
margin: 2em 0 10px; margin: 2em 0 10px;
opacity: 0.5; opacity: 0.5;
padding: 0 0.5em;
text-align: center;
width: fit-content;
&:hover { &:hover {
opacity: 1; opacity: 1;
} }

View file

@ -51,6 +51,7 @@
#V-SettingsPane { #V-SettingsPane {
height: calc(100% - 50px - @rlLowMargin); height: calc(100% - 50px - @rlLowMargin);
margin: 50px @rlLowMargin 0 0; margin: 50px @rlLowMargin 0 0;
width: 100%;
.btn-toolbar { .btn-toolbar {
position: absolute; position: absolute;

View file

@ -37,7 +37,7 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
import { ComposeAttachmentModel } from 'Model/ComposeAttachment'; import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
import { EmailModel } from 'Model/Email'; import { EmailModel, addressparser } from 'Model/Email';
import { decorateKoCommands, showScreenPopup } from 'Knoin/Knoin'; import { decorateKoCommands, showScreenPopup } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews'; import { AbstractViewPopup } from 'Knoin/AbstractViews';
@ -59,12 +59,7 @@ const
base64_encode = text => btoa(unescape(encodeURIComponent(text))).match(/.{1,76}/g).join('\r\n'), base64_encode = text => btoa(unescape(encodeURIComponent(text))).match(/.{1,76}/g).join('\r\n'),
email = new EmailModel(), getEmail = value => addressparser(value)[0]?.email || false,
getEmail = value => {
email.clear();
email.parse(value.trim());
return email.email || false;
},
/** /**
* @param {Array} aList * @param {Array} aList
@ -72,7 +67,7 @@ const
* @returns {string} * @returns {string}
*/ */
emailArrayToStringLineHelper = (aList, bFriendly) => emailArrayToStringLineHelper = (aList, bFriendly) =>
aList.map(item => item.toLine(bFriendly)).join(', '), aList.filter(item => item.email).map(item => item.toLine(bFriendly)).join(', '),
reloadDraftFolder = () => { reloadDraftFolder = () => {
const draftsFolder = FolderUserStore.draftsFolder(); const draftsFolder = FolderUserStore.draftsFolder();
@ -205,10 +200,6 @@ export class ComposePopupView extends AbstractViewPopup {
}; };
this.oEditor = null; this.oEditor = null;
this.aDraftInfo = null;
this.sInReplyTo = '';
this.bFromDraft = false;
this.sReferences = '';
this.sLastFocusedField = 'to'; this.sLastFocusedField = 'to';
@ -854,33 +845,17 @@ export class ComposePopupView extends AbstractViewPopup {
break; break;
case ComposeType.Draft: case ComposeType.Draft:
this.to(emailArrayToStringLineHelper(message.to));
this.cc(emailArrayToStringLineHelper(message.cc));
this.bcc(emailArrayToStringLineHelper(message.bcc));
this.replyTo(emailArrayToStringLineHelper(message.replyTo));
this.bFromDraft = true; this.bFromDraft = true;
this.draftsFolder(message.folder); this.draftsFolder(message.folder);
this.draftUid(message.uid); this.draftUid(message.uid);
// fallthrough
this.subject(sSubject);
this.prepareMessageAttachments(message, msgComposeType);
this.aDraftInfo = 3 === arrayLength(aDraftInfo) ? aDraftInfo : null;
this.sInReplyTo = message.inReplyTo;
this.sReferences = message.references;
break;
case ComposeType.EditAsNew: case ComposeType.EditAsNew:
this.to(emailArrayToStringLineHelper(message.to)); this.to(emailArrayToStringLineHelper(message.to));
this.cc(emailArrayToStringLineHelper(message.cc)); this.cc(emailArrayToStringLineHelper(message.cc));
this.bcc(emailArrayToStringLineHelper(message.bcc)); this.bcc(emailArrayToStringLineHelper(message.bcc));
this.replyTo(emailArrayToStringLineHelper(message.replyTo)); this.replyTo(emailArrayToStringLineHelper(message.replyTo));
this.subject(sSubject); this.subject(sSubject);
this.prepareMessageAttachments(message, msgComposeType); this.prepareMessageAttachments(message, msgComposeType);
this.aDraftInfo = 3 === arrayLength(aDraftInfo) ? aDraftInfo : null; this.aDraftInfo = 3 === arrayLength(aDraftInfo) ? aDraftInfo : null;
this.sInReplyTo = message.inReplyTo; this.sInReplyTo = message.inReplyTo;
this.sReferences = message.references; this.sReferences = message.references;

View file

@ -140,7 +140,7 @@ export class ContactsPopupView extends AbstractViewPopup {
this.contactsCheckedOrSelected().forEach(oContact => { this.contactsCheckedOrSelected().forEach(oContact => {
const data = oContact?.getNameAndEmailHelper(), const data = oContact?.getNameAndEmailHelper(),
email = data ? new EmailModel(data[0], data[1]) : null; email = data ? new EmailModel(data[0], data[1]) : null;
email?.validate() && aE.push(email); email?.valid() && aE.push(email);
}); });
if (arrayLength(aE)) { if (arrayLength(aE)) {

View file

@ -162,12 +162,12 @@ export class MailMessageList extends AbstractViewRight {
let list = [], current, sort = FolderUserStore.sortMode() || 'DATE'; let list = [], current, sort = FolderUserStore.sortMode() || 'DATE';
if (sort.includes('FROM')) { if (sort.includes('FROM')) {
MessagelistUserStore.forEach(msg => { MessagelistUserStore.forEach(msg => {
let email = msg.from?.[0].email; let email = msg.from[0].email;
if (!current || email != current.id) { if (!current || email != current.id) {
current = { current = {
id: email, id: email,
label: msg.from[0].toLine(), label: msg.from[0].toLine(),
search: 'from=' + msg.from[0].email, search: 'from=' + email,
messages: [] messages: []
}; };
list.push(current); list.push(current);
@ -235,7 +235,8 @@ export class MailMessageList extends AbstractViewRight {
this.selector.on('ItemSelect', message => { this.selector.on('ItemSelect', message => {
if (message) { if (message) {
populateMessageBody(MessageModel.fromMessageListItem(message)); // populateMessageBody(message.clone());
populateMessageBody(message);
} else { } else {
MessageUserStore.message(null); MessageUserStore.message(null);
} }

View file

@ -112,8 +112,7 @@ export class MailMessageView extends AbstractViewRight {
// viewer // viewer
viewFromShort: '', viewFromShort: '',
viewFromDkimData: ['none', ''], dkimData: ['none', '', '']
viewToShort: ''
}); });
this.moveAction = moveAction; this.moveAction = moveAction;
@ -171,10 +170,10 @@ export class MailMessageView extends AbstractViewRight {
canBeRepliedOrForwarded: () => !MessagelistUserStore.isDraftFolder() && this.messageVisibility(), canBeRepliedOrForwarded: () => !MessagelistUserStore.isDraftFolder() && this.messageVisibility(),
viewFromDkimVisibility: () => 'none' !== this.viewFromDkimData()[0], viewDkimIcon: () => 'none' !== this.dkimData()[0],
viewFromDkimStatusIconClass:() => { dkimIconClass:() => {
switch (this.viewFromDkimData()[0]) { switch (this.dkimData()[0]) {
case 'none': case 'none':
return ''; return '';
case 'pass': case 'pass':
@ -184,13 +183,9 @@ export class MailMessageView extends AbstractViewRight {
} }
}, },
viewFromDkimStatusTitle:() => { dkimTitle:() => {
const status = this.viewFromDkimData(); const dkim = this.dkimData();
if (arrayLength(status) && status[0]) { return dkim[0] ? dkim[2] || 'DKIM: ' + dkim[0] : '';
return status[1] || 'DKIM: ' + status[0];
}
return '';
}, },
showWhitelistOptions: () => 'match' === SettingsUserStore.viewImages(), showWhitelistOptions: () => 'match' === SettingsUserStore.viewImages(),
@ -212,10 +207,7 @@ export class MailMessageView extends AbstractViewRight {
this.viewHash = message.hash; this.viewHash = message.hash;
// TODO: make first param a user setting #683 // TODO: make first param a user setting #683
this.viewFromShort(message.from.toString(false, true)); this.viewFromShort(message.from.toString(false, true));
let dkim = 1 === arrayLength(message.from) && message.dkim this.dkimData(message.dkim[0] || ['none', '', '']);
&& message.dkim.find(dkim => message.from[0].email.includes(dkim[1]));
this.viewFromDkimData(dkim ? [dkim[0], dkim[2]] : ['none', '']);
this.viewToShort(message.to.toString(true, true));
} else { } else {
MessagelistUserStore.selectedMessage(null); MessagelistUserStore.selectedMessage(null);
@ -503,6 +495,11 @@ export class MailMessageView extends AbstractViewRight {
whitelistText(txt) { whitelistText(txt) {
let value = (SettingsUserStore.viewImagesWhitelist().trim() + '\n' + txt).trim(); let value = (SettingsUserStore.viewImagesWhitelist().trim() + '\n' + txt).trim();
/*
if ('pass' === currentMessage().spf[0]?.[0]) value += '+spf';
if ('pass' === currentMessage().dkim[0]?.[0]) value += '+dkim';
if ('pass' === currentMessage().dmarc[0]?.[0]) value += '+dmarc';
*/
SettingsUserStore.viewImagesWhitelist(value); SettingsUserStore.viewImagesWhitelist(value);
Remote.saveSetting('ViewImagesWhitelist', value); Remote.saveSetting('ViewImagesWhitelist', value);
currentMessage().showExternalImages(1); currentMessage().showExternalImages(1);

View file

@ -1,4 +1,4 @@
This app packages SnappyMail <upstream>2.25.5</upstream>. This app packages SnappyMail <upstream>2.26.1</upstream>.
SnappyMail is a simple, modern, lightweight & fast web-based email client. 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 WORKDIR /app/code
# If you change the extraction below, be sure to test on scaleway # If you change the extraction below, be sure to test on scaleway
VERSION=2.25.5 VERSION=2.26.1
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \ RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
unzip /tmp/snappymail.zip -d /app/code && \ unzip /tmp/snappymail.zip -d /app/code && \
rm /tmp/snappymail.zip && \ rm /tmp/snappymail.zip && \

View file

@ -32,6 +32,9 @@ From that point, all instance-wide SnappyMail settings can be tweaked as you wis
![grafik](https://user-images.githubusercontent.com/63400209/199768097-7bd939a7-56d0-47ba-b481-aeac08776fb4.png) ![grafik](https://user-images.githubusercontent.com/63400209/199768097-7bd939a7-56d0-47ba-b481-aeac08776fb4.png)
## App Integrations
### Contacts
SnappyMail automatically connects with the Nextcloud contacts app. Download and install the [contacts app](https://apps.nextcloud.com/apps/contacts) for SnappyMail to obtain access to all registered users on the Nextcloud system, as well as users' personal contacts saved in here.
## SnappyMail Settings, Where Are They? ## SnappyMail Settings, Where Are They?

View file

@ -3,7 +3,7 @@
<id>snappymail</id> <id>snappymail</id>
<name>SnappyMail</name> <name>SnappyMail</name>
<summary>SnappyMail Webmail</summary> <summary>SnappyMail Webmail</summary>
<version>2.25.5</version> <version>2.26.1</version>
<licence>agpl</licence> <licence>agpl</licence>
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author> <author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
<description><![CDATA[**Simple, modern, lightweight & fast web-based email client.** <description><![CDATA[**Simple, modern, lightweight & fast web-based email client.**

0
integrations/nextcloud/snappymail/appinfo/routes.php Executable file → Normal file
View file

View file

0
integrations/nextcloud/snappymail/templates/index.php Executable file → Normal file
View file

View file

View file

View file

@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
# script_snappymail_versions() # script_snappymail_versions()
sub script_snappymail_versions sub script_snappymail_versions
{ {
return ( "2.25.5" ); return ( "2.26.1" );
} }
sub script_snappymail_version_desc sub script_snappymail_version_desc

View file

@ -3,7 +3,7 @@
"title": "SnappyMail", "title": "SnappyMail",
"description": "Simple, modern & fast web-based email client", "description": "Simple, modern & fast web-based email client",
"private": true, "private": true,
"version": "2.25.5", "version": "2.26.1",
"homepage": "https://snappymail.eu", "homepage": "https://snappymail.eu",
"author": { "author": {
"name": "DJ Maze", "name": "DJ Maze",

View file

@ -4,11 +4,11 @@ class KolabPlugin extends \RainLoop\Plugins\AbstractPlugin
{ {
const const
NAME = 'Kolab', NAME = 'Kolab',
VERSION = '2.5', VERSION = '2.6',
RELEASE = '2023-01-17', RELEASE = '2023-02-10',
CATEGORY = 'Contacts', CATEGORY = 'Contacts',
DESCRIPTION = 'Use an Address Book of Kolab.', DESCRIPTION = 'Use an Address Book of Kolab.',
REQUIRED = '2.24.6'; REQUIRED = '2.26.0';
public function Init() : void public function Init() : void
{ {

View file

@ -46,25 +46,92 @@ class LdapMailAccounts
$this->Connect(); $this->Connect();
} }
/**
* @inheritDoc
*
* AOverwrite the MainAccount mail address by looking up the new one in the ldap directory
*
* The ldap search string has to be configured in the plugin configuration of the extension (in the SnappyMail Admin Panel)
*
* @param string &$sEmail
* @param string &$sLogin
*/
public function overwriteEmail(&$sEmail, &$sLogin)
{
try {
$this->EnsureBound();
} catch (LdapMailAccountsException $e) {
return false; // exceptions are only thrown from the handle error function that does logging already
}
// Try to get account information. IncLogin() returns the username of the user
// and removes the domainname if this was configured inside the domain config.
$username = $sEmail;
$oActions = \RainLoop\Api::Actions();
$oDomain = $oActions->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true);
if ($oDomain->IncShortLogin()){
$username = @ldap_escape($this->RemoveEventualDomainPart($sEmail), "", LDAP_ESCAPE_FILTER);
}
$searchString = $this->config->search_string;
// Replace placeholders inside the ldap search string with actual values
$searchString = str_replace("#USERNAME#", $username, $searchString);
$searchString = str_replace("#BASE_DN#", $this->config->base, $searchString);
$this->logger->Write("ldap search string after replacement of placeholders: $searchString", \LOG_NOTICE, self::LOG_KEY);
try {
$mailAddressResults = $this->FindLdapResults(
$this->config->field_search,
$searchString,
$this->config->base,
$this->config->objectclass,
$this->config->field_name,
$this->config->field_username,
$this->config->field_domain,
true,
$this->config->field_mail_address_main_account,
);
}
catch (LdapMailAccountsException $e) {
return false; // exceptions are only thrown from the handle error function that does logging already
}
if (count($mailAddressResults) < 1) {
$this->logger->Write("Could not find user $username in LDAP! Overwriting of main mail address not possible.", \LOG_NOTICE, self::LOG_KEY);
return false;
}
foreach($mailAddressResults as $mailAddressResult)
{
if($mailAddressResult->username === $username) {
//$sLogin is already set to be the same as $sEmail by function "resolveLoginCredentials" in /RainLoop/Actions/UserAuth.php
//that called this hook, so we just have to overwrite the mail address
$sEmail = $mailAddressResult->mailMainAccount;
}
}
}
/** /**
* @inheritDoc * @inheritDoc
* *
* Add additional mail accounts to the given primary account by looking up the ldap directory * Add additional mail accounts to the given primary account by looking up the ldap directory
* *
* The ldap lookup has to be configured in the plugin configuration of the extension (in the SnappyMail Admin Panel) * The ldap search string has to be configured in the plugin configuration of the extension (in the SnappyMail Admin Panel)
* *
* @param MainAccount $oAccount * @param MainAccount $oAccount
* @return bool true if additional accounts have been added or no additional accounts where found in . false if an error occured * @return bool true if additional accounts have been added or no additional accounts where found in ldap. false if an error occured
*/ */
public function AddLdapMailAccounts(MainAccount $oAccount): bool public function AddLdapMailAccounts(MainAccount $oAccount): bool
{ {
try { try {
$this->EnsureBound(); $this->EnsureBound();
} catch (LdapMailAccountsException $e) { } catch (LdapMailAccountsException $e) {
return false; // exceptions are only thrown from the handleerror function that does logging already return false; // exceptions are only thrown from the handle error function that does logging already
} }
// Try to get account information. Login() returns the username of the user // Try to get account information. IncLogin() returns the username of the user
// and removes the domainname if this was configured inside the domain config. // and removes the domainname if this was configured inside the domain config.
$username = @ldap_escape($oAccount->IncLogin(), "", LDAP_ESCAPE_FILTER); $username = @ldap_escape($oAccount->IncLogin(), "", LDAP_ESCAPE_FILTER);
@ -84,11 +151,13 @@ class LdapMailAccounts
$this->config->objectclass, $this->config->objectclass,
$this->config->field_name, $this->config->field_name,
$this->config->field_username, $this->config->field_username,
$this->config->field_domain $this->config->field_domain,
false,
$this->config->field_mail_address_additional_account
); );
} }
catch (LdapMailAccountsException $e) { catch (LdapMailAccountsException $e) {
return false; // exceptions are only thrown from the handleerror function that does logging already return false; // exceptions are only thrown from the handle error function that does logging already
} }
if (count($mailAddressResults) < 1) { if (count($mailAddressResults) < 1) {
$this->logger->Write("Could not find user $username", \LOG_NOTICE, self::LOG_KEY); $this->logger->Write("Could not find user $username", \LOG_NOTICE, self::LOG_KEY);
@ -109,7 +178,7 @@ class LdapMailAccounts
$aAccounts = $oActions->GetAccounts($oAccount); $aAccounts = $oActions->GetAccounts($oAccount);
//Search for accounts with suffix " (LDAP)" at the end of the name that where created by this plugin and initially remove them from the //Search for accounts with suffix " (LDAP)" at the end of the name that were created by this plugin and initially remove them from the
//account array. This only removes the visibility but does not delete the config done by the user. So if a user looses access to a //account array. This only removes the visibility but does not delete the config done by the user. So if a user looses access to a
//mailbox the user will not see the account anymore but the configuration can be restored when the user regains access to it //mailbox the user will not see the account anymore but the configuration can be restored when the user regains access to it
foreach($aAccounts as $key => $aAccount) foreach($aAccounts as $key => $aAccount)
@ -125,31 +194,35 @@ class LdapMailAccounts
$sUsername = $mailAddressResult->username; $sUsername = $mailAddressResult->username;
$sDomain = $mailAddressResult->domain; $sDomain = $mailAddressResult->domain;
$sName = $mailAddressResult->name; $sName = $mailAddressResult->name;
$sEmail = $mailAddressResult->mailAdditionalAccount;
//Check if the domain of the found mail address is in the list of configured domains //Check if the domain of the found mail address is in the list of configured domains
if ($oActions->DomainProvider()->Load($sDomain, true)) if ($oActions->DomainProvider()->Load($sDomain, true))
{ {
//only execute if the found account isn't already in the list of additional accounts //only execute if the found account isn't already in the list of additional accounts
//and if the found account is different from the main account //and if the found account is different from the main account.
if (!isset($aAccounts["$sUsername@$sDomain"]) && $oAccount->Email() !== "$sUsername@$sDomain") //The check if the address is different from the one of the main account when using the Nextcloud integration needs
//to be done twice: directly on the mail address (when Nextcloud is configured to log the user in by mail address)
//or on "$sUsername@$sDomain" for the case Nextcloud logs the user in to SnappyMail by his username and a default domain.
if (!isset($aAccounts[$sEmail]) && $oAccount->Email() !== $sEmail && $oAccount->Email() !== "$sUsername@$sDomain")
{ {
//Try to login the user with the same password as the primary account has //Try to login the user with the same password as the primary account has
//if this fails the user will see the new mail addresses but will be asked for the correct password //if this fails the user will see the new mail addresses but will be asked for the correct password
$sPass = $oAccount->IncPassword(); $sPass = $oAccount->IncPassword();
//After creating the accounts here $sUsername is used as username to login to the IMAP server (see Account.php)
$oNewAccount = RainLoop\Model\AdditionalAccount::NewInstanceFromCredentials($oActions, $sEmail, $sUsername, $sPass);
$oNewAccount = RainLoop\Model\AdditionalAccount::NewInstanceFromCredentials($oActions, "$sUsername@$sDomain", $sUsername, $sPass); $aAccounts[$sEmail] = $oNewAccount->asTokenArray($oAccount);
$aAccounts["$sUsername@$sDomain"] = $oNewAccount->asTokenArray($oAccount);
} }
//Always inject/update the found mailbox names into the array (also if the mailbox already existed) //Always inject/update the found mailbox names into the array (also if the mailbox already existed)
if (isset($aAccounts["$sUsername@$sDomain"])) if (isset($aAccounts[$sEmail]))
{ {
$aAccounts["$sUsername@$sDomain"]['name'] = $sName . " (LDAP)"; $aAccounts[$sEmail]['name'] = $sName . " (LDAP)";
} }
} }
else { else {
$this->logger->Write("Domain $sDomain is not part of configured domains in SnappyMail Admin Panel - mail address $sUsername@$sDomain will not be added.", \LOG_NOTICE, self::LOG_KEY); $this->logger->Write("Domain $sDomain is not part of configured domains in SnappyMail Admin Panel - mail address $sEmail will not be added.", \LOG_NOTICE, self::LOG_KEY);
} }
} }
@ -264,6 +337,8 @@ class LdapMailAccounts
* @param string $nameField * @param string $nameField
* @param string $usernameField * @param string $usernameField
* @param string $domainField * @param string $domainField
* @param bool $overwriteMailMainAccount true if the mail address of the main account should be looked up for overwriting. false if additional mail accounts should be searched
* @param string $mailAddressField The field containing the mail address (of main account or additional mail account)
* @return LdapMailAccountResult[] * @return LdapMailAccountResult[]
* @throws LdapMailAccountsException * @throws LdapMailAccountsException
*/ */
@ -274,7 +349,9 @@ class LdapMailAccounts
string $objectClass, string $objectClass,
string $nameField, string $nameField,
string $usernameField, string $usernameField,
string $domainField): array string $domainField,
bool $overwriteMailMainAccount,
string $mailAddressField): array
{ {
$this->EnsureBound(); $this->EnsureBound();
$nameField = strtolower($nameField); $nameField = strtolower($nameField);
@ -282,9 +359,12 @@ class LdapMailAccounts
$domainField = strtolower($domainField); $domainField = strtolower($domainField);
$filter = "(&(objectclass=$objectClass)($searchField=$searchString))"; $filter = "(&(objectclass=$objectClass)($searchField=$searchString))";
$this->logger->Write("Used ldap filter to search for additional mail accounts: $filter", \LOG_NOTICE, self::LOG_KEY); $this->logger->Write("Used ldap filter to search for mail account(s): $filter", \LOG_NOTICE, self::LOG_KEY);
$ldapResult = @ldap_search($this->ldap, $searchBase, $filter, ['dn', $usernameField, $nameField, $domainField]); //Set together the attributes to search inside the LDAP
$ldapAttributes = ['dn', $usernameField, $nameField, $domainField, $mailAddressField];
$ldapResult = @ldap_search($this->ldap, $searchBase, $filter, $ldapAttributes);
if (!$ldapResult) { if (!$ldapResult) {
$this->HandleLdapError("Fetch $objectClass"); $this->HandleLdapError("Fetch $objectClass");
return []; return [];
@ -311,6 +391,13 @@ class LdapMailAccounts
$result->domain = $this->LdapGetAttribute($entry, $domainField, true, true); $result->domain = $this->LdapGetAttribute($entry, $domainField, true, true);
$result->domain = $this->RemoveEventualLocalPart($result->domain); $result->domain = $this->RemoveEventualLocalPart($result->domain);
if($overwriteMailMainAccount) {
$result->mailMainAccount = $this->LdapGetAttribute($entry, $mailAddressField, true, true);
}
else {
$result->mailAdditionalAccount = $this->LdapGetAttribute($entry, $mailAddressField, true, true);
}
$results[] = $result; $results[] = $result;
} }
@ -407,4 +494,10 @@ class LdapMailAccountResult
/** @var string */ /** @var string */
public $domain; public $domain;
/** @var string */
public $mailMainAccount;
/** @var string */
public $mailAdditionalAccount;
} }

View file

@ -18,6 +18,9 @@ class LdapMailAccountsConfig
public const CONFIG_FIELD_USERNAME = "field_username"; public const CONFIG_FIELD_USERNAME = "field_username";
public const CONFIG_SEARCH_STRING = "search_string"; public const CONFIG_SEARCH_STRING = "search_string";
public const CONFIG_FIELD_MAIL_DOMAIN = "field_domain"; public const CONFIG_FIELD_MAIL_DOMAIN = "field_domain";
public const CONFIG_FIELD_MAIL_ADDRESS_ADDITIONAL_ACCOUNT = "field_mail_address_additional_account";
public const CONFIG_BOOL_OVERWRITE_MAIL_ADDRESS_MAIN_ACCOUNT = "bool_overwrite_mail_address_main_account";
public const CONFIG_FIELD_MAIL_ADDRESS_MAIN_ACCOUNT = "field_mail_address_main_account";
public $server; public $server;
public $protocol; public $protocol;
@ -30,6 +33,9 @@ class LdapMailAccountsConfig
public $field_username; public $field_username;
public $search_string; public $search_string;
public $field_domain; public $field_domain;
public $field_mail_address_main_account;
public $field_mail_address_additional_account;
public $bool_overwrite_mail_address_main_account;
public static function MakeConfig(Plugin $config): LdapMailAccountsConfig public static function MakeConfig(Plugin $config): LdapMailAccountsConfig
{ {
@ -45,6 +51,9 @@ class LdapMailAccountsConfig
$ldap->field_username = trim($config->Get("plugin", self::CONFIG_FIELD_USERNAME)); $ldap->field_username = trim($config->Get("plugin", self::CONFIG_FIELD_USERNAME));
$ldap->search_string = trim($config->Get("plugin", self::CONFIG_SEARCH_STRING)); $ldap->search_string = trim($config->Get("plugin", self::CONFIG_SEARCH_STRING));
$ldap->field_domain = trim($config->Get("plugin", self::CONFIG_FIELD_MAIL_DOMAIN)); $ldap->field_domain = trim($config->Get("plugin", self::CONFIG_FIELD_MAIL_DOMAIN));
$ldap->field_mail_address_additional_account = trim($config->Get("plugin", self::CONFIG_FIELD_MAIL_ADDRESS_ADDITIONAL_ACCOUNT));
$ldap->bool_overwrite_mail_address_main_account = $config->Get("plugin", self::CONFIG_BOOL_OVERWRITE_MAIL_ADDRESS_MAIN_ACCOUNT);
$ldap->field_mail_address_main_account = trim($config->Get("plugin", self::CONFIG_FIELD_MAIL_ADDRESS_MAIN_ACCOUNT));
return $ldap; return $ldap;
} }

View file

@ -4,6 +4,10 @@
This plugin can be used to add additional mail accounts to SnappyMail when a user logs in successfully. The list of additional accounts is retrieved by a ldap query that can be configured inside the plugin settings.\ This plugin can be used to add additional mail accounts to SnappyMail when a user logs in successfully. The list of additional accounts is retrieved by a ldap query that can be configured inside the plugin settings.\
On a successful login the username of the SnappyMail user is passed to the plugin and will be searched in the ldap. If additional mail accounts are found, the username and domain-part of those will be used to add the new mail account. The plugin tries to log in the user with the same password used to login to SnappyMail - if this fails SnappyMail asks the user to insert his credentials. On a successful login the username of the SnappyMail user is passed to the plugin and will be searched in the ldap. If additional mail accounts are found, the username and domain-part of those will be used to add the new mail account. The plugin tries to log in the user with the same password used to login to SnappyMail - if this fails SnappyMail asks the user to insert his credentials.
Version 2.0.0 changes the way additional mail accounts get their e-mail address: the mail address connected with additional mail accounts is now always the address found inside the ldap.
Now it is also possible to overwrite the mail address of the main account: if a user logs into SnappyMail with a username and SnappyMail added the configured default domain the mail address of the main account could have been some not existing address like "username@default-domain.com". This could have happend when using the Nextcloud SnappyMail integration that offers an automatic login using the Nextcloud username.
The plugin now can be configured to overwrite the username or mail address used at login with a mail address found inside ldap.
### Configuration ### Configuration
- Install and activate the plugin using the SnappyMail Admin Panel -> menu Extensions. - Install and activate the plugin using the SnappyMail Admin Panel -> menu Extensions.
- Click on the gear symbol beside the plugin to open the configration dialog. - Click on the gear symbol beside the plugin to open the configration dialog.
@ -11,7 +15,9 @@ On a successful login the username of the SnappyMail user is passed to the plugi
- The fields `Object class`, `Base DN`, `Search field` and `LDAP search string` are used to put together a ldap search filter which will return the additional mail accounts of a user:\ - The fields `Object class`, `Base DN`, `Search field` and `LDAP search string` are used to put together a ldap search filter which will return the additional mail accounts of a user:\
`(&(objectclass=<YOUR OBJECT CLASS>)(<YOUR SEARCH FIELD>=<YOUR SEARCH STRING>))`.\ `(&(objectclass=<YOUR OBJECT CLASS>)(<YOUR SEARCH FIELD>=<YOUR SEARCH STRING>))`.\
This filter will be executed on the `Base DN` you have defined. `LDAP search string` can contain the placeholders `#USERNAME#` (will be replaced with the username the user logged in to Snappymail) and `#BASE_DN#` (will be replaced with the value you inserted into the field `Base DN` inside the plugin settings). This will allow you to create more complex search strings like `uid=#USERNAME#`. This filter will be executed on the `Base DN` you have defined. `LDAP search string` can contain the placeholders `#USERNAME#` (will be replaced with the username the user logged in to Snappymail) and `#BASE_DN#` (will be replaced with the value you inserted into the field `Base DN` inside the plugin settings). This will allow you to create more complex search strings like `uid=#USERNAME#`.
- `Username field of additional account`, `Domain name field of additional account` and `Additional account name field` are used to define the ldap attributes to read when the ldap search was successful. For example insert `mail` into the `Username field of additional account` and the `Domain name field of additional account` to use the [local-part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the mail address as username and the [domain-part](https://en.wikipedia.org/wiki/Email_address#Domain) as domain for the additional account.\ - `Username field`, `Domain name field of additional account`, `Mail address field for additional account` and `Additional account name field` are used to define the ldap attributes to read when the ldap search was successful. For example insert `mail` into the `Username field` and the `Domain name field of additional account` to use the [local-part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the mail address as username and the [domain-part](https://en.wikipedia.org/wiki/Email_address#Domain) as domain for the additional account.\
`Username field of additional account` and `Domain name field of additional account` before use get checked by the plugin if they contain a mail address and if true only the local-part or domain-part is returned. If no `@` is found the content of the found ldap attribute is returned without modification. This can be usefull if your user should login with something different than the mail address (a username that is diffrent from the local-part of the mail address). `Username field` and `Domain name field of additional account` before use get checked by the plugin if they contain a mail address and if true only the local-part or domain-part is returned. If no `@` is found the content of the found ldap attribute is returned without modification. This can be usefull if your user should login with something different than the mail address (a username that is diffrent from the local-part of the mail address).
**Important:** keep in mind, that SnappyMail normally needs a mail address as username. In some special circumstances (login with an ldap username, not a mail address) this can mean that you have to configure the plugin to put together a 'fake' mail address like `<LDAP-USERNAME>@<YOUR-DOMAIN>`. Section `Overwrite mail address of main account` can be used to overwrite the username or mail address used at login with a value found in ldap. If activated, the username or mail address used at login will be looked up inside the `Username field` in ldap (for details see how a search for additional accounts is made). If the username is found, the value of the field `Mail address field for main account` will be used to overwrite the mail address of the main account.
**Important:** SnappyMail normally needs a mail address as username. This plugin handles some special circumstances (login with an ldap username, not a mail address) so that you can login to your IMAP server with the ldap username but send mails with a mail address connected to this ldap user.

View file

@ -12,11 +12,11 @@ class LdapMailAccountsPlugin extends AbstractPlugin
{ {
const const
NAME = 'LDAP Mail Accounts', NAME = 'LDAP Mail Accounts',
VERSION = '1.1', VERSION = '2.0.0',
AUTHOR = 'cm-schl', AUTHOR = 'cm-schl',
URL = 'https://github.com/cm-sch', URL = 'https://github.com/cm-sch',
RELEASE = '2022-12-08', RELEASE = '2022-12-08',
REQUIRED = '2.23.0', REQUIRED = '2.25.4',
CATEGORY = 'Accounts', CATEGORY = 'Accounts',
DESCRIPTION = 'Add additional mail accounts the SnappyMail user has access to by a LDAP query. Basing on the work of FWest98 (https://github.com/FWest98).'; DESCRIPTION = 'Add additional mail accounts the SnappyMail user has access to by a LDAP query. Basing on the work of FWest98 (https://github.com/FWest98).';
@ -32,6 +32,30 @@ class LdapMailAccountsPlugin extends AbstractPlugin
public function Init(): void public function Init(): void
{ {
$this->addHook("login.success", 'AddAdditionalLdapMailAccounts'); $this->addHook("login.success", 'AddAdditionalLdapMailAccounts');
$this->addHook('login.credentials', 'overwriteMainAccountEmail');
}
// Function gets called by RainLoop/Actions/UserAuth.php
/**
* Overwrite the MainAccount mail address by looking up the new one in the ldap directory
*
* @param string &$sEmail
* @param string &$sLogin
*/
public function overwriteMainAccountEmail(&$sEmail, &$sLogin)
{
$this->Manager()->Actions()->Logger()->Write("Login DATA: login: $sLogin email: $sEmail", \LOG_WARNING, "LDAP MAIL ACCOUNTS PLUGIN");
// Set up config
$config = LdapMailAccountsConfig::MakeConfig($this->Config());
if ($config->bool_overwrite_mail_address_main_account)
{
$oldapMailAccounts = new LdapMailAccounts($config, $this->Manager()->Actions()->Logger());
$oldapMailAccounts->overwriteEmail($sEmail, $sLogin);
}
$this->Manager()->Actions()->Logger()->Write("Login DATA: login: $sLogin email: $sEmail", \LOG_WARNING, "LDAP MAIL ACCOUNTS PLUGIN");
} }
// Function gets called by RainLoop/Actions/User.php // Function gets called by RainLoop/Actions/User.php
@ -44,9 +68,7 @@ class LdapMailAccountsPlugin extends AbstractPlugin
{ {
// Set up config // Set up config
$config = LdapMailAccountsConfig::MakeConfig($this->Config()); $config = LdapMailAccountsConfig::MakeConfig($this->Config());
$oldapMailAccounts = new LdapMailAccounts($config, $this->Manager()->Actions()->Logger()); $oldapMailAccounts = new LdapMailAccounts($config, $this->Manager()->Actions()->Logger());
$oldapMailAccounts->AddLdapMailAccounts($oAccount); $oldapMailAccounts->AddLdapMailAccounts($oAccount);
} }
@ -55,73 +77,102 @@ class LdapMailAccountsPlugin extends AbstractPlugin
*/ */
protected function configMapping(): array protected function configMapping(): array
{ {
$groupOverwriteMainAccount = new \RainLoop\Plugins\PropertyCollection('Overwrite mail address of main account');
$groupOverwriteMainAccount->exchangeArray([
\RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_BOOL_OVERWRITE_MAIL_ADDRESS_MAIN_ACCOUNT)->SetLabel('Enabled')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(false),
\RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_MAIL_ADDRESS_MAIN_ACCOUNT)
->SetLabel("Mail address field for main account")
->SetType(RainLoop\Enumerations\PluginPropertyType::STRING)
->SetDescription("The ldap field containing the mail address to use on the SnappyMail main account.
\nThe value found inside ldap will overwrite the mail address of the SnappyMail main account (the account the user logged in at SnappyMail)
\nThe mail address used at login will still be used to login to the servers.")
->SetDefaultValue("mail"),
]);
return [ return [
Property::NewInstance(LdapMailAccountsConfig::CONFIG_SERVER) \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_SERVER)
->SetLabel("LDAP Server URL") ->SetLabel("LDAP Server URL")
->SetPlaceholder("ldap://server:port") ->SetPlaceholder("ldap://server:port")
->SetType(PluginPropertyType::STRING), ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING),
Property::NewInstance(LdapMailAccountsConfig::CONFIG_PROTOCOL_VERSION) \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_PROTOCOL_VERSION)
->SetLabel("LDAP Protocol Version") ->SetLabel("LDAP Protocol Version")
->SetType(PluginPropertyType::SELECTION) ->SetType(RainLoop\Enumerations\PluginPropertyType::SELECTION)
->SetDefaultValue([2, 3]), ->SetDefaultValue([2, 3]),
Property::NewInstance(LdapMailAccountsConfig::CONFIG_BIND_USER) \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_BIND_USER)
->SetLabel("LDAP Username") ->SetLabel("LDAP Username")
->SetDescription("The user to use for binding to the LDAP server. Should be a DN or RDN. Leave empty for anonymous bind.") ->SetDescription("The user to use for binding to the LDAP server. Should be a DN or RDN. Leave empty for anonymous bind.")
->SetType(PluginPropertyType::STRING), ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING),
Property::NewInstance(LdapMailAccountsConfig::CONFIG_BIND_PASSWORD) \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_BIND_PASSWORD)
->SetLabel("LDAP Password") ->SetLabel("LDAP Password")
->SetDescription("Leave empty for anonymous bind.") ->SetDescription("Leave empty for anonymous bind.")
->SetType(PluginPropertyType::PASSWORD), ->SetType(RainLoop\Enumerations\PluginPropertyType::PASSWORD),
Property::NewInstance(LdapMailAccountsConfig::CONFIG_OBJECTCLASS) \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_OBJECTCLASS)
->SetLabel("Object class") ->SetLabel("Object class")
->SetType(PluginPropertyType::STRING) ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING)
->SetDescription("The object class to use when searching for additional mail accounts of the logged in SnappyMail user") ->SetDescription("The object class to use when searching for additional mail accounts of the logged in SnappyMail user")
->SetDefaultValue("user"), ->SetDefaultValue("user"),
Property::NewInstance(LdapMailAccountsConfig::CONFIG_BASE) \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_BASE)
->SetLabel("Base DN") ->SetLabel("Base DN")
->SetType(PluginPropertyType::STRING) ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING)
->SetDescription("The base DN to search in for additional mail accounts of the logged in SnappyMail user"), ->SetDescription("The base DN to search in for additional mail accounts of the logged in SnappyMail user"),
Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_SEARCH) \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_SEARCH)
->SetLabel("Search field") ->SetLabel("Search field")
->SetType(PluginPropertyType::STRING) ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING)
->SetDescription("The name of the ldap attribute that has to contain the here defined 'LDAP search string'.") ->SetDescription("The name of the ldap attribute that has to contain the here defined 'LDAP search string'.")
->SetDefaultValue("member"), ->SetDefaultValue("member"),
Property::NewInstance(LdapMailAccountsConfig::CONFIG_SEARCH_STRING) \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_SEARCH_STRING)
->SetLabel("LDAP search string") ->SetLabel("LDAP search string")
->SetType(PluginPropertyType::STRING) ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING)
->SetDescription("The search string used to find ldap objects of mail accounts the user has access to. ->SetDescription("The search string used to find ldap objects of mail accounts the user has access to.
\nPossible placeholers:\n#USERNAME# - replaced with the username of the actual SnappyMail user \nPossible placeholers:\n#USERNAME# - replaced with the username of the actual SnappyMail user
\n#BASE_DN# - replaced with the value inside the field 'User base DN'.") \n#BASE_DN# - replaced with the value inside the field 'User base DN'.")
->SetDefaultValue("uid=#USERNAME#"), ->SetDefaultValue("uid=#USERNAME#"),
Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_USERNAME) \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_USERNAME)
->SetLabel("Username field of additional account") ->SetLabel("Username field")
->SetType(PluginPropertyType::STRING) ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING)
->SetDescription("The field containing the username of the found additional mail account. ->SetDescription("Used when searching for additional accounts or when overwriting the mail address of the main account.
\nThis username gets used by SnappyMail to login to the additional mail account. \nThe field containing the username of the mail account.
\nIf this field contains an email address, only the local-part before the @ is used.") \nWhen looking up additional accounts:
\nIf this field contains an email address, only the local-part before the @ is used. The domain part is retrieved configuring the field below. This username gets used by SnappyMail to login to the additional mail account
\nWhen overwriting the main account mail address:
\nThe username from SnappyMail login gets used to search an LDAP entry containig a field with the same username.")
->SetDefaultValue("uid"), ->SetDefaultValue("uid"),
Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_MAIL_DOMAIN) \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_MAIL_DOMAIN)
->SetLabel("Domain name field of additional account") ->SetLabel("Domain name field of additional account")
->SetType(PluginPropertyType::STRING) ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING)
->SetDescription("The field containing the domain name of the found additional mail account. ->SetDescription("The field containing the domain name of the found additional mail account.
\nThis domain gets looked up by SnappyMail to choose the right connection parameters at logging in to the additional mail account. \nThis domain gets looked up by SnappyMail to choose the right connection parameters at logging in to the additional mail account.
\nIf this field contains an email address, only the domain-part after the @ is used.") \nIf this field contains an email address, only the domain-part after the @ is used.")
->SetDefaultValue("mail"), ->SetDefaultValue("mail"),
Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_NAME) \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_MAIL_ADDRESS_ADDITIONAL_ACCOUNT)
->SetLabel("Mail address field for additional account")
->SetType(RainLoop\Enumerations\PluginPropertyType::STRING)
->SetDescription("The ldap field containing the mail address to use on the found additional mail account.
\nThe value found inside ldap will be used as mail address of the additional mail accounts created by this plugin.
\nIn most cases this could be the same ldap field as in \"Domain name field of additional account\"")
->SetDefaultValue("mail"),
\RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_NAME)
->SetLabel("Additional account name field") ->SetLabel("Additional account name field")
->SetType(PluginPropertyType::STRING) ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING)
->SetDescription("The field containing the default sender name of the found additional mail account.") ->SetDescription("The field containing the default sender name of the found additional mail account.")
->SetDefaultValue("displayName") ->SetDefaultValue("displayName"),
$groupOverwriteMainAccount
]; ];
} }
} }

View file

@ -97,7 +97,7 @@ abstract class HtmlUtils
$oBody = $oDoc->getElementsByTagName('body')->item(0); $oBody = $oDoc->getElementsByTagName('body')->item(0);
$aRemoveTags = array( $aRemoveTags = array(
'svg', 'link', 'base', 'meta', 'title', 'x-script', 'script', 'bgsound', 'keygen', 'source', 'svg', 'link', 'base', 'meta', 'title', 'script', 'bgsound', 'keygen', 'source',
'object', 'embed', 'applet', 'mocha', 'iframe', 'frame', 'frameset', 'video', 'audio', 'area', 'map', 'object', 'embed', 'applet', 'mocha', 'iframe', 'frame', 'frameset', 'video', 'audio', 'area', 'map',
'head', 'style' 'head', 'style'
); );

View file

@ -185,7 +185,7 @@ class Http
static::$bCache = true; static::$bCache = true;
\header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); \header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
\header('Last-Modified: '.\gmdate('D, d M Y H:i:s').' GMT'); \header('Last-Modified: '.\gmdate('D, d M Y H:i:s').' GMT');
\header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0'); \header('Cache-Control: no-store');
\header('Pragma: no-cache'); \header('Pragma: no-cache');
} }
} }

View file

@ -617,6 +617,7 @@ class ImapClient extends \MailSo\Net\NetClient
} }
/* /*
// literal-string // literal-string
$this->hasCapability('LITERAL+')
if (\preg_match('/[\r\n\x00\x80-\xFF]/', $sStringForEscape)) { if (\preg_match('/[\r\n\x00\x80-\xFF]/', $sStringForEscape)) {
return \sprintf("{%d}\r\n%s", \strlen($sStringForEscape), $sStringForEscape); return \sprintf("{%d}\r\n%s", \strlen($sStringForEscape), $sStringForEscape);
} }

View file

@ -67,9 +67,7 @@ class ResponseCollection extends \MailSo\Base\Collection
} }
if (\is_array($aList) && \count($aList)) { if (\is_array($aList) && \count($aList)) {
$aList = \array_map('strtoupper', $aList); return \array_map('strtoupper', $aList);
\sort($aList);
return $aList;
} }
} }
return null; return null;

View file

@ -316,8 +316,12 @@ abstract class SearchCriterias
public static function escapeSearchString(\MailSo\Imap\ImapClient $oImapClient, string $sSearch) : string public static function escapeSearchString(\MailSo\Imap\ImapClient $oImapClient, string $sSearch) : string
{ {
return !\MailSo\Base\Utils::IsAscii($sSearch) // https://github.com/the-djmaze/snappymail/issues/836
? '{'.\strlen($sSearch).'}'."\r\n".$sSearch : $oImapClient->EscapeString($sSearch); // return $oImapClient->EscapeString($sSearch);
// return \MailSo\Base\Utils::IsAscii($sSearch) || $oImapClient->hasCapability('QQMail'))
return (\MailSo\Base\Utils::IsAscii($sSearch) || !$oImapClient->hasCapability('LITERAL+'))
? $oImapClient->EscapeString($sSearch)
: '{'.\strlen($sSearch).'}'."\r\n{$sSearch}";
} }
private static function parseSearchDate(string $sDate) : int private static function parseSearchDate(string $sDate) : int

View file

@ -28,7 +28,7 @@ class Email implements \JsonSerializable
*/ */
function __construct(string $sEmail, string $sDisplayName = '') function __construct(string $sEmail, string $sDisplayName = '')
{ {
if (!\strlen(\trim($sEmail))) { if (!\strlen(\trim($sEmail)) && !\strlen(\trim($sDisplayName))) {
throw new \InvalidArgumentException; throw new \InvalidArgumentException;
} }

View file

@ -76,12 +76,9 @@ class HeaderCollection extends \MailSo\Base\Collection
public function GetAsEmailCollection(string $sHeaderName, bool $bCharsetAutoDetect = false) : ?EmailCollection public function GetAsEmailCollection(string $sHeaderName, bool $bCharsetAutoDetect = false) : ?EmailCollection
{ {
$oResult = null; return new EmailCollection(
$sValue = $this->ValueByName($sHeaderName, $bCharsetAutoDetect); $this->ValueByName($sHeaderName, $bCharsetAutoDetect)
if (\strlen($sValue)) { );
$oResult = new EmailCollection($sValue);
}
return $oResult && $oResult->count() ? $oResult : null;
} }
public function ParametersByName(string $sHeaderName) : ?ParameterCollection public function ParametersByName(string $sHeaderName) : ?ParameterCollection

View file

@ -179,16 +179,19 @@ class Message extends Part
public function SetInReplyTo(string $sInReplyTo) : self public function SetInReplyTo(string $sInReplyTo) : self
{ {
$this->aHeadersValue[Enumerations\Header::IN_REPLY_TO] = $sInReplyTo; $sInReplyTo = \trim($sInReplyTo);
if (\strlen($sInReplyTo)) {
$this->aHeadersValue[Enumerations\Header::IN_REPLY_TO] = $sInReplyTo;
}
return $this; return $this;
} }
public function SetReferences(string $sReferences) : self public function SetReferences(string $sReferences) : self
{ {
$this->aHeadersValue[Enumerations\Header::REFERENCES] = $sReferences = \MailSo\Base\Utils::StripSpaces($sReferences);
\MailSo\Base\Utils::StripSpaces($sReferences); if (\strlen($sReferences)) {
$this->aHeadersValue[Enumerations\Header::REFERENCES] = $sReferences;
}
return $this; return $this;
} }
@ -239,8 +242,9 @@ class Message extends Part
public function SetTo(EmailCollection $oEmails) : self public function SetTo(EmailCollection $oEmails) : self
{ {
$this->aHeadersValue[Enumerations\Header::TO_] = $oEmails; if ($oEmails->count()) {
$this->aHeadersValue[Enumerations\Header::TO_] = $oEmails;
}
return $this; return $this;
} }
@ -253,22 +257,25 @@ class Message extends Part
public function SetReplyTo(EmailCollection $oEmails) : self public function SetReplyTo(EmailCollection $oEmails) : self
{ {
$this->aHeadersValue[Enumerations\Header::REPLY_TO] = $oEmails; if ($oEmails->count()) {
$this->aHeadersValue[Enumerations\Header::REPLY_TO] = $oEmails;
}
return $this; return $this;
} }
public function SetCc(EmailCollection $oEmails) : self public function SetCc(EmailCollection $oEmails) : self
{ {
$this->aHeadersValue[Enumerations\Header::CC] = $oEmails; if ($oEmails->count()) {
$this->aHeadersValue[Enumerations\Header::CC] = $oEmails;
}
return $this; return $this;
} }
public function SetBcc(EmailCollection $oEmails) : self public function SetBcc(EmailCollection $oEmails) : self
{ {
$this->aHeadersValue[Enumerations\Header::BCC] = $oEmails; if ($oEmails->count()) {
$this->aHeadersValue[Enumerations\Header::BCC] = $oEmails;
}
return $this; return $this;
} }

View file

@ -92,7 +92,8 @@ abstract class NetClient
} }
if ($this->IsConnected()) { if ($this->IsConnected()) {
$this->writeLogException(new Exceptions\SocketAlreadyConnectedException, \LOG_ERR); $this->writeLogException(new Exceptions\SocketAlreadyConnectedException, \LOG_ERR, false);
return;
} }
$this->Settings = $oSettings; $this->Settings = $oSettings;
@ -214,14 +215,6 @@ abstract class NetClient
return false; return false;
} }
/**
* @throws \MailSo\Net\Exceptions\SocketConnectionDoesNotAvailableException
*/
public function IsConnectedWithException() : void
{
$this->IsConnected(true);
}
public function StreamContextParams() : array public function StreamContextParams() : array
{ {
return $this->rConnect && \MailSo\Base\Utils::FunctionCallable('stream_context_get_options') return $this->rConnect && \MailSo\Base\Utils::FunctionCallable('stream_context_get_options')

View file

@ -689,6 +689,7 @@ class Actions
'ViewImages' => 'ask', 'ViewImages' => 'ask',
'ViewImagesWhitelist' => '', 'ViewImagesWhitelist' => '',
'RemoveColors' => (bool) $oConfig->Get('defaults', 'remove_colors', false), 'RemoveColors' => (bool) $oConfig->Get('defaults', 'remove_colors', false),
'AllowStyles' => false,
'ListInlineAttachments' => false, 'ListInlineAttachments' => false,
'CollapseBlockquotes' => true, 'CollapseBlockquotes' => true,
'MaxBlockquotesLevel' => 0, 'MaxBlockquotesLevel' => 0,
@ -801,6 +802,7 @@ class Actions
$aResult['ViewImages'] = $oSettings->GetConf('ViewImages', $show_images ? 'always' : 'ask'); $aResult['ViewImages'] = $oSettings->GetConf('ViewImages', $show_images ? 'always' : 'ask');
$aResult['ViewImagesWhitelist'] = $oSettings->GetConf('ViewImagesWhitelist', ''); $aResult['ViewImagesWhitelist'] = $oSettings->GetConf('ViewImagesWhitelist', '');
$aResult['RemoveColors'] = (bool)$oSettings->GetConf('RemoveColors', $aResult['RemoveColors']); $aResult['RemoveColors'] = (bool)$oSettings->GetConf('RemoveColors', $aResult['RemoveColors']);
$aResult['AllowStyles'] = (bool)$oSettings->GetConf('AllowStyles', $aResult['AllowStyles']);
$aResult['ListInlineAttachments'] = (bool)$oSettings->GetConf('ListInlineAttachments', $aResult['ListInlineAttachments']); $aResult['ListInlineAttachments'] = (bool)$oSettings->GetConf('ListInlineAttachments', $aResult['ListInlineAttachments']);
$aResult['CollapseBlockquotes'] = (bool)$oSettings->GetConf('CollapseBlockquotes', $aResult['CollapseBlockquotes']); $aResult['CollapseBlockquotes'] = (bool)$oSettings->GetConf('CollapseBlockquotes', $aResult['CollapseBlockquotes']);
$aResult['MaxBlockquotesLevel'] = (int)$oSettings->GetConf('MaxBlockquotesLevel', $aResult['MaxBlockquotesLevel']); $aResult['MaxBlockquotesLevel'] = (int)$oSettings->GetConf('MaxBlockquotesLevel', $aResult['MaxBlockquotesLevel']);

View file

@ -989,10 +989,7 @@ trait Messages
$oFrom = $oMessage->GetFrom(); $oFrom = $oMessage->GetFrom();
$oMessage->RegenerateMessageId($oFrom ? $oFrom->GetDomain() : ''); $oMessage->RegenerateMessageId($oFrom ? $oFrom->GetDomain() : '');
$oReplyTo = new \MailSo\Mime\EmailCollection($this->GetActionParam('replyTo', '')); $oMessage->SetReplyTo(new \MailSo\Mime\EmailCollection($this->GetActionParam('replyTo', '')));
if ($oReplyTo->count()) {
$oMessage->SetReplyTo($oReplyTo);
}
if (!empty($this->GetActionParam('readReceiptRequest', 0))) { if (!empty($this->GetActionParam('readReceiptRequest', 0))) {
// Read Receipts Reference Main Account Email, Not Identities #147 // Read Receipts Reference Main Account Email, Not Identities #147
@ -1010,35 +1007,17 @@ trait Messages
$oMessage->SetSubject($this->GetActionParam('subject', '')); $oMessage->SetSubject($this->GetActionParam('subject', ''));
$oToEmails = new \MailSo\Mime\EmailCollection($this->GetActionParam('to', '')); $oMessage->SetTo(new \MailSo\Mime\EmailCollection($this->GetActionParam('to', '')));
if ($oToEmails->count()) { $oMessage->SetCc(new \MailSo\Mime\EmailCollection($this->GetActionParam('cc', '')));
$oMessage->SetTo($oToEmails); $oMessage->SetBcc(new \MailSo\Mime\EmailCollection($this->GetActionParam('bcc', '')));
}
$oCcEmails = new \MailSo\Mime\EmailCollection($this->GetActionParam('cc', ''));
if ($oCcEmails->count()) {
$oMessage->SetCc($oCcEmails);
}
$oBccEmails = new \MailSo\Mime\EmailCollection($this->GetActionParam('bcc', ''));
if ($oBccEmails->count()) {
$oMessage->SetBcc($oBccEmails);
}
$aDraftInfo = $this->GetActionParam('draftInfo', null); $aDraftInfo = $this->GetActionParam('draftInfo', null);
if ($bWithDraftInfo && \is_array($aDraftInfo) && !empty($aDraftInfo[0]) && !empty($aDraftInfo[1]) && !empty($aDraftInfo[2])) { if ($bWithDraftInfo && \is_array($aDraftInfo) && !empty($aDraftInfo[0]) && !empty($aDraftInfo[1]) && !empty($aDraftInfo[2])) {
$oMessage->SetDraftInfo($aDraftInfo[0], $aDraftInfo[1], $aDraftInfo[2]); $oMessage->SetDraftInfo($aDraftInfo[0], $aDraftInfo[1], $aDraftInfo[2]);
} }
$sInReplyTo = $this->GetActionParam('inReplyTo', ''); $oMessage->SetInReplyTo($this->GetActionParam('inReplyTo', ''));
if (\strlen($sInReplyTo)) { $oMessage->SetReferences($this->GetActionParam('references', ''));
$oMessage->SetInReplyTo($sInReplyTo);
}
$sReferences = $this->GetActionParam('references', '');
if (\strlen($sReferences)) {
$oMessage->SetReferences($sReferences);
}
$aFoundCids = array(); $aFoundCids = array();
$aFoundDataURL = array(); $aFoundDataURL = array();

View file

@ -15,7 +15,7 @@ trait Themes
&& ($oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount))) { && ($oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount))) {
$sTheme = (string) $oSettingsLocal->GetConf('Theme', $sTheme); $sTheme = (string) $oSettingsLocal->GetConf('Theme', $sTheme);
} }
$sTheme = $this->ValidateTheme($sTheme) ?: 'Default'; $sTheme = $this->ValidateTheme($sTheme);
} }
return $sTheme; return $sTheme;
} }
@ -92,7 +92,13 @@ trait Themes
public function ValidateTheme(string $sTheme): string public function ValidateTheme(string $sTheme): string
{ {
return \in_array($sTheme, $this->GetThemes()) ? $sTheme : $this->Config()->Get('themes', 'default', 'Default'); if (!\in_array($sTheme, $this->GetThemes())) {
$sTheme = $this->Config()->Get('webmail', 'theme', 'Default');
if (!\in_array($sTheme, $this->GetThemes())) {
$sTheme = 'Default';
}
}
return $sTheme;
} }
public function compileCss(string $sTheme, bool $bAdmin, bool $bMinified = false) : string public function compileCss(string $sTheme, bool $bAdmin, bool $bMinified = false) : string

View file

@ -181,6 +181,7 @@ trait User
$this->setSettingsFromParams($oSettings, 'ViewImages', 'string'); $this->setSettingsFromParams($oSettings, 'ViewImages', 'string');
$this->setSettingsFromParams($oSettings, 'ViewImagesWhitelist', 'string'); $this->setSettingsFromParams($oSettings, 'ViewImagesWhitelist', 'string');
$this->setSettingsFromParams($oSettings, 'RemoveColors', 'bool'); $this->setSettingsFromParams($oSettings, 'RemoveColors', 'bool');
$this->setSettingsFromParams($oSettings, 'AllowStyles', 'bool');
$this->setSettingsFromParams($oSettings, 'ListInlineAttachments', 'bool'); $this->setSettingsFromParams($oSettings, 'ListInlineAttachments', 'bool');
$this->setSettingsFromParams($oSettings, 'CollapseBlockquotes', 'bool'); $this->setSettingsFromParams($oSettings, 'CollapseBlockquotes', 'bool');
$this->setSettingsFromParams($oSettings, 'MaxBlockquotesLevel', 'int'); $this->setSettingsFromParams($oSettings, 'MaxBlockquotesLevel', 'int');

View file

@ -6,50 +6,24 @@ use MailSo\Base\Utils;
class Identity implements \JsonSerializable class Identity implements \JsonSerializable
{ {
/** private string $sId;
* @var string
*/
private $sId;
/** private string $sEmail;
* @var string
*/
private $sEmail;
/** private string $sName = '';
* @var string
*/
private $sName;
/** private string $sReplyTo = '';
* @var string
*/
private $sReplyTo;
/** private string $sBcc = '';
* @var string
*/
private $sBcc;
/** private string $sSignature = '';
* @var string
*/
private $sSignature;
/** private bool $bSignatureInsertBefore = false;
* @var bool
*/
private $bSignatureInsertBefore;
function __construct(string $sId = '', string $sEmail = '') function __construct(string $sId = '', string $sEmail = '')
{ {
$this->sId = $sId; $this->sId = $sId;
$this->sEmail = $sEmail; $this->sEmail = $sEmail;
$this->sName = '';
$this->sReplyTo = '';
$this->sBcc = '';
$this->sSignature = '';
$this->bSignatureInsertBefore = false;
} }
public function Id(bool $bFillOnEmpty = false): string public function Id(bool $bFillOnEmpty = false): string
@ -84,16 +58,6 @@ class Identity implements \JsonSerializable
return $this->sBcc; return $this->sBcc;
} }
public function Signature(): string
{
return $this->sSignature;
}
public function SignatureInsertBefore(): bool
{
return $this->bSignatureInsertBefore;
}
public function SetId(string $sId): Identity public function SetId(string $sId): Identity
{ {
$this->sId = $sId; $this->sId = $sId;
@ -151,13 +115,13 @@ class Identity implements \JsonSerializable
public function ToSimpleJSON(): array public function ToSimpleJSON(): array
{ {
return array( return array(
'Id' => $this->Id(), 'Id' => $this->sId,
'Email' => $this->Email(), 'Email' => $this->sEmail,
'Name' => $this->Name(), 'Name' => $this->sName,
'ReplyTo' => $this->ReplyTo(), 'ReplyTo' => $this->sReplyTo,
'Bcc' => $this->Bcc(), 'Bcc' => $this->sBcc,
'Signature' => $this->Signature(), 'Signature' => $this->sSignature,
'SignatureInsertBefore' => $this->SignatureInsertBefore() 'SignatureInsertBefore' => $this->bSignatureInsertBefore
); );
} }
@ -165,13 +129,13 @@ class Identity implements \JsonSerializable
public function jsonSerialize() public function jsonSerialize()
{ {
return array( return array(
'Id' => $this->Id(), 'Id' => $this->sId,
'Email' => Utils::IdnToUtf8($this->Email()), 'Email' => Utils::IdnToUtf8($this->sEmail),
'Name' => $this->Name(), 'Name' => $this->sName,
'ReplyTo' => $this->ReplyTo(), 'ReplyTo' => $this->sReplyTo,
'Bcc' => $this->Bcc(), 'Bcc' => $this->sBcc,
'Signature' => $this->Signature(), 'Signature' => $this->sSignature,
'SignatureInsertBefore' => $this->SignatureInsertBefore() 'SignatureInsertBefore' => $this->bSignatureInsertBefore
); );
} }

View file

@ -551,7 +551,7 @@ class ServiceActions
{ {
$this->oHttp->ServerNoCache(); $this->oHttp->ServerNoCache();
$sTo = \trim($_GET['to'] ?? ''); $sTo = \trim($_GET['to'] ?? '');
if (!empty($sTo) && \preg_match('/^mailto:/i', $sTo)) { if (\preg_match('/^mailto:/i', $sTo)) {
\SnappyMail\Cookies::set( \SnappyMail\Cookies::set(
Actions::AUTH_MAILTO_TOKEN_KEY, Actions::AUTH_MAILTO_TOKEN_KEY,
Utils::EncodeKeyValuesQ(array( Utils::EncodeKeyValuesQ(array(

View file

@ -9,7 +9,7 @@ class CSP
{ {
public public
$base = ["'self'"], $base = ["'self'"],
$default = ["'self'"], $default = ["'self'", 'data:'],
// Knockout.js requires eval() for observable binding purposes // Knockout.js requires eval() for observable binding purposes
// Safari < 15.4 does not support strict-dynamic // Safari < 15.4 does not support strict-dynamic
// $script = ["'strict-dynamic'", "'unsafe-eval'"], // $script = ["'strict-dynamic'", "'unsafe-eval'"],

View file

@ -29,7 +29,7 @@ abstract class Stream
// We just fake Drupal https://www.drupal.org/docs/8/core/modules/big-pipe/bigpipe-environment-requirements // We just fake Drupal https://www.drupal.org/docs/8/core/modules/big-pipe/bigpipe-environment-requirements
\header('Surrogate-Control: no-store, content="BigPipe/1.0"'); \header('Surrogate-Control: no-store, content="BigPipe/1.0"');
// Explicitly disable caching so Varnish and other upstreams won't cache. // Explicitly disable caching so Varnish and other upstreams won't cache.
\header('Cache-Control: no-store, no-cache, must-revalidate'); \header('Cache-Control: no-store');
\header('Pragma: no-cache'); \header('Pragma: no-cache');
// Nginx: disable fastcgi_buffering and disable gzip for this request. // Nginx: disable fastcgi_buffering and disable gzip for this request.

View file

@ -77,7 +77,7 @@ class ZIP
while ($i-- && \ob_end_clean()); while ($i-- && \ob_end_clean());
\ob_get_level() || \header('Content-Encoding: '); \ob_get_level() || \header('Content-Encoding: ');
} }
\header('Cache-Control: no-store, no-cache, must-revalidate'); \header('Cache-Control: no-store');
\header('Pragma: no-cache'); \header('Pragma: no-cache');
\header('Content-Transfer-Encoding: binary'); \header('Content-Transfer-Encoding: binary');
$name = "{$name}.zip"; $name = "{$name}.zip";

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "الرسائل في الصفحة", "MESSAGE_PER_PAGE": "الرسائل في الصفحة",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Съобщения на страница", "MESSAGE_PER_PAGE": "Съобщения на страница",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Zpráv na stranu", "MESSAGE_PER_PAGE": "Zpráv na stranu",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Meddelelser per side", "MESSAGE_PER_PAGE": "Meddelelser per side",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "HTML gegenüber Klartext bevorzugen", "PREFER_HTML": "HTML gegenüber Klartext bevorzugen",
"PREFER_HTML_INFO": "Nachrichten können manchmal in beiden Formaten empfangen werden. Diese Option setzt, ob die Nachricht standardmäßig in HTML oder als Klartext angezeigt wird.", "PREFER_HTML_INFO": "Nachrichten können manchmal in beiden Formaten empfangen werden. Diese Option setzt, ob die Nachricht standardmäßig in HTML oder als Klartext angezeigt wird.",
"REMOVE_COLORS": "Entferne Hintergrund- und Textfarben aus der Nachricht", "REMOVE_COLORS": "Entferne Hintergrund- und Textfarben aus der Nachricht",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Nachrichten pro Seite", "MESSAGE_PER_PAGE": "Nachrichten pro Seite",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Nachricht als gelesen markieren nach", "MARK_MESSAGE_READ_AFTER": "Nachricht als gelesen markieren nach",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Μηνύματα στη σελίδα", "MESSAGE_PER_PAGE": "Μηνύματα στη σελίδα",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Messages on page", "MESSAGE_PER_PAGE": "Messages on page",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Messages on page", "MESSAGE_PER_PAGE": "Messages on page",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -19,7 +19,7 @@
"MOVE_TO": "Mover", "MOVE_TO": "Mover",
"NAME": "Nombre", "NAME": "Nombre",
"NONE": "Ninguno", "NONE": "Ninguno",
"NOT_SPAM": "Correo no deseado", "NOT_SPAM": "Es correo deseado",
"USERNAME": "Nombre de usuario", "USERNAME": "Nombre de usuario",
"PASSWORD": "Contraseña", "PASSWORD": "Contraseña",
"REMEMBER": "Remember", "REMEMBER": "Remember",
@ -27,7 +27,7 @@
"SAVE": "Guardar", "SAVE": "Guardar",
"SAVE_CHANGES": "Save changes?", "SAVE_CHANGES": "Save changes?",
"SEARCH": "Buscar", "SEARCH": "Buscar",
"SPAM": "Es correo deseado", "SPAM": "Correo no deseado",
"SUBJECT": "Asunto", "SUBJECT": "Asunto",
"TEST": "Probar", "TEST": "Probar",
"TO": "Para", "TO": "Para",
@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Eliminar el fondo y los colores del texto del mensaje", "REMOVE_COLORS": "Eliminar el fondo y los colores del texto del mensaje",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Mensajes en página", "MESSAGE_PER_PAGE": "Mensajes en página",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Marcar el mensaje como leído después de", "MARK_MESSAGE_READ_AFTER": "Marcar el mensaje como leído después de",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Lehel kuvatavate kirjade arv", "MESSAGE_PER_PAGE": "Lehel kuvatavate kirjade arv",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Lehenetsi HTMLa testu arruntari", "PREFER_HTML": "Lehenetsi HTMLa testu arruntari",
"PREFER_HTML_INFO": "Mezuak batzuetan bi formatuetan datoz. Aukera honekin HTML edo testu arrunt partea erakustea aukera dezakezu.", "PREFER_HTML_INFO": "Mezuak batzuetan bi formatuetan datoz. Aukera honekin HTML edo testu arrunt partea erakustea aukera dezakezu.",
"REMOVE_COLORS": "Kendu atzealdeko irudia eta testuaren kolorean mezuaren gorputzetik", "REMOVE_COLORS": "Kendu atzealdeko irudia eta testuaren kolorean mezuaren gorputzetik",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Orrialdeko mezuak", "MESSAGE_PER_PAGE": "Orrialdeko mezuak",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Markatu mezua irakurrita ondoren", "MARK_MESSAGE_READ_AFTER": "Markatu mezua irakurrita ondoren",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "پیام‌ها در صفحه", "MESSAGE_PER_PAGE": "پیام‌ها در صفحه",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Viestiä sivulla", "MESSAGE_PER_PAGE": "Viestiä sivulla",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -173,11 +173,11 @@
"CONTACTS": { "CONTACTS": {
"TAB_CONTACT": "Contact", "TAB_CONTACT": "Contact",
"TAB_LOCATIONS": "Localisations", "TAB_LOCATIONS": "Localisations",
"TAB_BUSINESS": "Métier", "TAB_BUSINESS": "Professionel",
"LABEL_ORGANIZATION": "Entreprise", "LABEL_ORGANIZATION": "Entreprise",
"LABEL_DEPARTMENT": "Department", "LABEL_DEPARTMENT": "Service",
"LABEL_TITLE": "Titre", "LABEL_TITLE": "Titre",
"TAB_CRYPTO": "Chiffre", "TAB_CRYPTO": "Chiffrement",
"ASK": "Demander", "ASK": "Demander",
"NEVER": "Jamais", "NEVER": "Jamais",
"ALWAYS": "Toujours", "ALWAYS": "Toujours",
@ -403,6 +403,7 @@
"PREFER_HTML": "Préférer le HTML au texte brut", "PREFER_HTML": "Préférer le HTML au texte brut",
"PREFER_HTML_INFO": "Les messages arrivent parfois dans les deux formats. Cette option contrôle si vous voulez que la partie HTML ou la partie texte brut soit affichée.", "PREFER_HTML_INFO": "Les messages arrivent parfois dans les deux formats. Cette option contrôle si vous voulez que la partie HTML ou la partie texte brut soit affichée.",
"REMOVE_COLORS": "Supprimer les couleurs d'arrière-plan et de texte du message", "REMOVE_COLORS": "Supprimer les couleurs d'arrière-plan et de texte du message",
"ALLOW_STYLES": "Autoriser <style> CSS",
"MESSAGE_PER_PAGE": "Messages par page", "MESSAGE_PER_PAGE": "Messages par page",
"CHECK_MAIL_INTERVAL": "Vérifier le messagerie à l'intervalle", "CHECK_MAIL_INTERVAL": "Vérifier le messagerie à l'intervalle",
"MARK_MESSAGE_READ_AFTER": "Marquer le message comme lu après", "MARK_MESSAGE_READ_AFTER": "Marquer le message comme lu après",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "Images externes", "EXTERNAL_IMAGES": "Images externes",
"IMAGES_OPTION_ASK": "Toujours demander", "IMAGES_OPTION_ASK": "Toujours demander",
"IMAGES_OPTION_WHITELIST": "Ajouter à la liste blanche ou demander", "IMAGES_OPTION_WHITELIST": "Ajouter à la liste blanche ou demander",
"IMAGES_OPTION_DKIM": "Toujours afficher quand DKIM est valide",
"IMAGES_OPTION_ALWAYS": "Toujours afficher", "IMAGES_OPTION_ALWAYS": "Toujours afficher",
"IMAGES_WHITELIST": "Liste blanche Images" "IMAGES_WHITELIST": "Liste blanche Images"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Üzenetek egy oldalon", "MESSAGE_PER_PAGE": "Üzenetek egy oldalon",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Jelölje meg az üzenetet olvasottként", "MARK_MESSAGE_READ_AFTER": "Jelölje meg az üzenetet olvasottként",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Pesan di halaman", "MESSAGE_PER_PAGE": "Pesan di halaman",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Skilaboð á síðu", "MESSAGE_PER_PAGE": "Skilaboð á síðu",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -6,7 +6,7 @@
"TEST": "Test", "TEST": "Test",
"UPDATE": "Aggiorna", "UPDATE": "Aggiorna",
"USERNAME": "Nome utente", "USERNAME": "Nome utente",
"PASSWORD": "Parola d'ordine", "PASSWORD": "Password",
"CANCEL": "Annulla" "CANCEL": "Annulla"
}, },
"LOGIN": { "LOGIN": {

View file

@ -21,7 +21,7 @@
"NONE": "Nessuno", "NONE": "Nessuno",
"NOT_SPAM": "Segnala come non spam", "NOT_SPAM": "Segnala come non spam",
"USERNAME": "Nome utente", "USERNAME": "Nome utente",
"PASSWORD": "Parola d'ordine", "PASSWORD": "Password",
"REMEMBER": "Remember", "REMEMBER": "Remember",
"REPLY_TO": "Rispondi a", "REPLY_TO": "Rispondi a",
"SAVE": "Salva", "SAVE": "Salva",
@ -403,6 +403,7 @@
"PREFER_HTML": "Usa HTML predefinito anziché formato testo", "PREFER_HTML": "Usa HTML predefinito anziché formato testo",
"PREFER_HTML_INFO": "Il messaggio a volte contiene entrambi i formati. Questa opzione agisce sulla visualizzazione del messaggio in HTML o della versione in formato testo.", "PREFER_HTML_INFO": "Il messaggio a volte contiene entrambi i formati. Questa opzione agisce sulla visualizzazione del messaggio in HTML o della versione in formato testo.",
"REMOVE_COLORS": "Rimuovi lo sfondo e i colori del testo dal messaggio", "REMOVE_COLORS": "Rimuovi lo sfondo e i colori del testo dal messaggio",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Messaggi per pagina", "MESSAGE_PER_PAGE": "Messaggi per pagina",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Contrassegna il messaggio come letto dopo", "MARK_MESSAGE_READ_AFTER": "Contrassegna il messaggio come letto dopo",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "プレーンテキストよりHTMLを優先する", "PREFER_HTML": "プレーンテキストよりHTMLを優先する",
"PREFER_HTML_INFO": "メッセージがHTMLとプレーンテキストの両方の形式を含むことがあります。このオプションはそのような場合にどちらの形式を表示するかを制御します。", "PREFER_HTML_INFO": "メッセージがHTMLとプレーンテキストの両方の形式を含むことがあります。このオプションはそのような場合にどちらの形式を表示するかを制御します。",
"REMOVE_COLORS": "本文の文字色及び背景色の設定を無視する", "REMOVE_COLORS": "本文の文字色及び背景色の設定を無視する",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "1ページに表示する件数", "MESSAGE_PER_PAGE": "1ページに表示する件数",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "メッセージが既読となるまでの秒数", "MARK_MESSAGE_READ_AFTER": "メッセージが既読となるまでの秒数",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "페이지 당 메시지 개수", "MESSAGE_PER_PAGE": "페이지 당 메시지 개수",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Žinučių lape", "MESSAGE_PER_PAGE": "Žinučių lape",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Ziņojumi lapā", "MESSAGE_PER_PAGE": "Ziņojumi lapā",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Foretrekk HTML fremfor", "PREFER_HTML": "Foretrekk HTML fremfor",
"PREFER_HTML_INFO": "Meldinger kan ankomme i både HTML og ren tekst. Dette valget styrer hvilken av dem du vil se.", "PREFER_HTML_INFO": "Meldinger kan ankomme i både HTML og ren tekst. Dette valget styrer hvilken av dem du vil se.",
"REMOVE_COLORS": "Fjern bakgrunn og tekstfarger fra brødtekst", "REMOVE_COLORS": "Fjern bakgrunn og tekstfarger fra brødtekst",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Meldinger per side", "MESSAGE_PER_PAGE": "Meldinger per side",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Marker melding som lest etter", "MARK_MESSAGE_READ_AFTER": "Marker melding som lest etter",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Geef de voorkeur aan HTML boven platte tekst", "PREFER_HTML": "Geef de voorkeur aan HTML boven platte tekst",
"PREFER_HTML_INFO": "Berichten komen soms in beide formaten. Deze optie bepaalt of u het HTML-gedeelte of het platte tekstgedeelte wilt weergeven.", "PREFER_HTML_INFO": "Berichten komen soms in beide formaten. Deze optie bepaalt of u het HTML-gedeelte of het platte tekstgedeelte wilt weergeven.",
"REMOVE_COLORS": "Verwijder achtergrond- en tekstkleuren uit het bericht", "REMOVE_COLORS": "Verwijder achtergrond- en tekstkleuren uit het bericht",
"ALLOW_STYLES": "CSS <style> toestaan",
"MESSAGE_PER_PAGE": "Berichten op pagina", "MESSAGE_PER_PAGE": "Berichten op pagina",
"CHECK_MAIL_INTERVAL": "Controleer mail interval", "CHECK_MAIL_INTERVAL": "Controleer mail interval",
"MARK_MESSAGE_READ_AFTER": "Bericht markeren als gelezen na", "MARK_MESSAGE_READ_AFTER": "Bericht markeren als gelezen na",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Wiadomości na stronę", "MESSAGE_PER_PAGE": "Wiadomości na stronę",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Mensagens por Página", "MESSAGE_PER_PAGE": "Mensagens por Página",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Preferir HTML em vez de texto simples", "PREFER_HTML": "Preferir HTML em vez de texto simples",
"PREFER_HTML_INFO": "Por vezes as mensagens são recebidas em ambos os formatos. Esta opção determina se prefere apresentar o conteúdo em HTML ou em texto simples.", "PREFER_HTML_INFO": "Por vezes as mensagens são recebidas em ambos os formatos. Esta opção determina se prefere apresentar o conteúdo em HTML ou em texto simples.",
"REMOVE_COLORS": "Retirar imagens de fundo e coloração de texto do corpo das mensagens", "REMOVE_COLORS": "Retirar imagens de fundo e coloração de texto do corpo das mensagens",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Mensagens por página", "MESSAGE_PER_PAGE": "Mensagens por página",
"CHECK_MAIL_INTERVAL": "Intervalo de verificação do correio", "CHECK_MAIL_INTERVAL": "Intervalo de verificação do correio",
"MARK_MESSAGE_READ_AFTER": "Marcar mensagem como lida após", "MARK_MESSAGE_READ_AFTER": "Marcar mensagem como lida após",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "Imagens externas", "EXTERNAL_IMAGES": "Imagens externas",
"IMAGES_OPTION_ASK": "Perguntar sempre", "IMAGES_OPTION_ASK": "Perguntar sempre",
"IMAGES_OPTION_WHITELIST": "Lista Branca ou perguntar", "IMAGES_OPTION_WHITELIST": "Lista Branca ou perguntar",
"IMAGES_OPTION_DKIM": "Mostrar sempre que DKIM for válido",
"IMAGES_OPTION_ALWAYS": "Mostrar sempre", "IMAGES_OPTION_ALWAYS": "Mostrar sempre",
"IMAGES_WHITELIST": "Lista Branca de imagens" "IMAGES_WHITELIST": "Lista Branca de imagens"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Preferir HTML em vez de texto simples", "PREFER_HTML": "Preferir HTML em vez de texto simples",
"PREFER_HTML_INFO": "Por vezes as mensagens são recebidas em ambos os formatos. Esta opção determina se prefere apresentar o conteúdo em HTML ou em texto simples.", "PREFER_HTML_INFO": "Por vezes as mensagens são recebidas em ambos os formatos. Esta opção determina se prefere apresentar o conteúdo em HTML ou em texto simples.",
"REMOVE_COLORS": "Retirar imagens de fundo e coloração de texto do corpo das mensagens", "REMOVE_COLORS": "Retirar imagens de fundo e coloração de texto do corpo das mensagens",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Mensagens por página", "MESSAGE_PER_PAGE": "Mensagens por página",
"CHECK_MAIL_INTERVAL": "Intervalo de verificação do correio", "CHECK_MAIL_INTERVAL": "Intervalo de verificação do correio",
"MARK_MESSAGE_READ_AFTER": "Marcar mensagem como lida após", "MARK_MESSAGE_READ_AFTER": "Marcar mensagem como lida após",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "Imagens externas", "EXTERNAL_IMAGES": "Imagens externas",
"IMAGES_OPTION_ASK": "Perguntar sempre", "IMAGES_OPTION_ASK": "Perguntar sempre",
"IMAGES_OPTION_WHITELIST": "Lista Branca ou perguntar", "IMAGES_OPTION_WHITELIST": "Lista Branca ou perguntar",
"IMAGES_OPTION_DKIM": "Mostrar sempre que DKIM for válido",
"IMAGES_OPTION_ALWAYS": "Mostrar sempre", "IMAGES_OPTION_ALWAYS": "Mostrar sempre",
"IMAGES_WHITELIST": "Lista Branca de imagens" "IMAGES_WHITELIST": "Lista Branca de imagens"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Mesaje pe o pagină", "MESSAGE_PER_PAGE": "Mesaje pe o pagină",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Cообщений на одной странице", "MESSAGE_PER_PAGE": "Cообщений на одной странице",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Správ na stranu", "MESSAGE_PER_PAGE": "Správ na stranu",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Sporočil na strani", "MESSAGE_PER_PAGE": "Sporočil na strani",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Meddelanden per sida", "MESSAGE_PER_PAGE": "Meddelanden per sida",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Markera meddelandet som läst efter", "MARK_MESSAGE_READ_AFTER": "Markera meddelandet som läst efter",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Messages on page", "MESSAGE_PER_PAGE": "Messages on page",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Повідомлень на одній сторінці", "MESSAGE_PER_PAGE": "Повідомлень на одній сторінці",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Nghiêng về lựa chọn HTML hơn dạng chữ viết thường", "PREFER_HTML": "Nghiêng về lựa chọn HTML hơn dạng chữ viết thường",
"PREFER_HTML_INFO": "Đôi lúc thư nhận được sẽ ở cả hai dạng. Lựa chọn này cho phép bạn chỉnh phần HTML hay phần chữ viết thường trong thư bạn muốn hiển thị lên.", "PREFER_HTML_INFO": "Đôi lúc thư nhận được sẽ ở cả hai dạng. Lựa chọn này cho phép bạn chỉnh phần HTML hay phần chữ viết thường trong thư bạn muốn hiển thị lên.",
"REMOVE_COLORS": "Loại bỏ hình nền và màu chữ trong phần thân của thư", "REMOVE_COLORS": "Loại bỏ hình nền và màu chữ trong phần thân của thư",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "Số thư hiển thị trên trang", "MESSAGE_PER_PAGE": "Số thư hiển thị trên trang",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Đánh dấu thư là để đọc sau", "MARK_MESSAGE_READ_AFTER": "Đánh dấu thư là để đọc sau",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "相比纯文本优先使用 HTML", "PREFER_HTML": "相比纯文本优先使用 HTML",
"PREFER_HTML_INFO": "消息有时以两种格式传送。该选项控制您希望显示 HTML 部分还是纯文本部分。", "PREFER_HTML_INFO": "消息有时以两种格式传送。该选项控制您希望显示 HTML 部分还是纯文本部分。",
"REMOVE_COLORS": "从消息中删除背景和文本颜色", "REMOVE_COLORS": "从消息中删除背景和文本颜色",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "每页消息数", "MESSAGE_PER_PAGE": "每页消息数",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "将消息标记为已读", "MARK_MESSAGE_READ_AFTER": "将消息标记为已读",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -403,6 +403,7 @@
"PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML": "Prefer HTML to plain text",
"PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.",
"REMOVE_COLORS": "Remove background and text colors from message body", "REMOVE_COLORS": "Remove background and text colors from message body",
"ALLOW_STYLES": "Allow <style> CSS",
"MESSAGE_PER_PAGE": "封郵件每頁", "MESSAGE_PER_PAGE": "封郵件每頁",
"CHECK_MAIL_INTERVAL": "Check mail interval", "CHECK_MAIL_INTERVAL": "Check mail interval",
"MARK_MESSAGE_READ_AFTER": "Mark message as read after", "MARK_MESSAGE_READ_AFTER": "Mark message as read after",
@ -421,7 +422,6 @@
"EXTERNAL_IMAGES": "External images", "EXTERNAL_IMAGES": "External images",
"IMAGES_OPTION_ASK": "Always ask", "IMAGES_OPTION_ASK": "Always ask",
"IMAGES_OPTION_WHITELIST": "Whitelist or ask", "IMAGES_OPTION_WHITELIST": "Whitelist or ask",
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
"IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_OPTION_ALWAYS": "Show always",
"IMAGES_WHITELIST": "Images whitelist" "IMAGES_WHITELIST": "Images whitelist"
}, },

View file

@ -6,7 +6,7 @@
<i data-bind="css: collapsedCss()"></i> <i data-bind="css: collapsedCss()"></i>
<!-- ko text: name --><!-- /ko --> <!-- ko text: name --><!-- /ko -->
</a> </a>
<!-- ko if: subFolders.length --> <!-- ko if: subFolders().length -->
<ul data-bind="template: { name: 'MailFolderListItem', foreach: subFolders }, css: { 'collapsed': collapsed() }"></ul> <ul data-bind="template: { name: 'MailFolderListItem', foreach: subFolders }, css: { 'collapsed': collapsed() }"></ul>
<!-- /ko --> <!-- /ko -->
</li> </li>

View file

@ -31,7 +31,7 @@
</div> </div>
<!-- ko if: message --> <!-- ko if: message -->
<div class="b-message" data-bind="i18nUpdate: message, css: message().lineAsCss(0)"> <div class="b-message" data-bind="i18nUpdate: message, css: message()?.lineAsCss(0)">
<div class="message-fixed-button-toolbar"> <div class="message-fixed-button-toolbar">
<a class="btn btn-success buttonEdit fontastic hide-mobile" data-bind="visible: isDraftFolder(), command: editCommand">🖉</a> <a class="btn btn-success buttonEdit fontastic hide-mobile" data-bind="visible: isDraftFolder(), command: editCommand">🖉</a>
<div class="btn-group" style="margin-right: -8px; display: inline;"> <div class="btn-group" style="margin-right: -8px; display: inline;">
@ -130,14 +130,14 @@
</div> </div>
<div class="informationShort" data-bind="hidden: showFullInfo"> <div class="informationShort" data-bind="hidden: showFullInfo">
<span class="from" data-bind="html: viewFromShort, title: message().from"></span> <span class="from" data-bind="html: viewFromShort, title: message().from"></span>
<i data-bind="visible: viewFromDkimVisibility, css: viewFromDkimStatusIconClass, title: viewFromDkimStatusTitle"></i> <i data-bind="visible: viewDkimIcon, css: dkimIconClass, title: dkimTitle"></i>
</div> </div>
<div class="informationFull" data-bind="visible: showFullInfo, with: message"> <div class="informationFull" data-bind="visible: showFullInfo, with: message">
<table> <table>
<tr data-bind="visible: from.length"> <tr data-bind="visible: from.length">
<td data-i18n="GLOBAL/FROM"></td> <td data-i18n="GLOBAL/FROM"></td>
<td><span data-bind="text: from"></span> <td><span data-bind="text: from"></span>
<i data-bind="visible: $parent.viewFromDkimVisibility, css: $parent.viewFromDkimStatusIconClass, title: $parent.viewFromDkimStatusTitle"></i> <i data-bind="visible: $parent.viewDkimIcon, css: $parent.dkimIconClass, title: $parent.dkimTitle"></i>
</td> </td>
</tr> </tr>
<tr data-bind="visible: to.length"> <tr data-bind="visible: to.length">
@ -279,9 +279,9 @@
</span> </span>
</div> </div>
<div class="showImages" data-bind="visible: message().hasImages()"> <div class="btn-toolbar showImages" data-bind="visible: message().hasImages()">
<div class="btn" data-bind="click: showImages" data-icon="🖼" data-i18n="MESSAGE/BUTTON_SHOW_IMAGES"></div> <div class="btn" data-bind="click: showImages" data-icon="🖼" data-i18n="MESSAGE/BUTTON_SHOW_IMAGES"></div>
<div class="btn-group" data-bind="registerBootstrapDropdown: true, visible: showWhitelistOptions" style="display: inline-block"> <div class="btn-group dropdown" data-bind="registerBootstrapDropdown: true, visible: showWhitelistOptions" style="display: inline-block">
<a class="btn dropdown-toggle" id="whitelist-dropdown-id" data-icon="🖼" href="#" tabindex="-1"><span data-i18n="SETTINGS_GENERAL/IMAGES_WHITELIST"></span></a> <a class="btn dropdown-toggle" id="whitelist-dropdown-id" data-icon="🖼" href="#" tabindex="-1"><span data-i18n="SETTINGS_GENERAL/IMAGES_WHITELIST"></span></a>
<menu class="dropdown-menu" role="menu" aria-labelledby="whitelist-dropdown-id"> <menu class="dropdown-menu" role="menu" aria-labelledby="whitelist-dropdown-id">
<!-- ko foreach: message().whitelistOptions() --> <!-- ko foreach: message().whitelistOptions() -->

Some files were not shown because too many files have changed in this diff Show more