Merge branch 'the-djmaze:master' into rework-on-mailaddress-handling

This commit is contained in:
cm-schl 2023-02-15 12:26:35 +01:00 committed by GitHub
commit aa03e84c79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
107 changed files with 972 additions and 1018 deletions

View file

@ -316,5 +316,5 @@ dev_email = ""
dev_password = "" dev_password = ""
[version] [version]
current = "2.25.4" 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,101 @@
## 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
## Added
- New dark themes by @TheCuteFoxxy
[#925](https://github.com/the-djmaze/snappymail/pull/925)
- External images option 'Always when DKIM is valid' for
[#201](https://github.com/the-djmaze/snappymail/pull/201)
- Image whitelist menu on message for
[#201](https://github.com/the-djmaze/snappymail/pull/201)
## Changed
- pt-PT translation by @ner00
[#917](https://github.com/the-djmaze/snappymail/pull/917) and
[#920](https://github.com/the-djmaze/snappymail/pull/920)
- fr-FR translation by @hguilbert
[#919](https://github.com/the-djmaze/snappymail/pull/919)
- Layout section rl-right now is `display: flex` and solves
[#928](https://github.com/the-djmaze/snappymail/pull/928)
- Some Themes background to `cover`
[#918](https://github.com/the-djmaze/snappymail/pull/918)
- Speedup cleanHtml() parser
- Reduce memory usage on addressbook import
## Fixed
- Prevent loading loop between MessageList and FolderInformation requests
- Admin -> Config layout
- Inline images sometimes failed
- Undefined index: cid
[#921](https://github.com/the-djmaze/snappymail/pull/921)
- On upgrade prevent Apache access errors for
[#358](https://github.com/the-djmaze/snappymail/pull/358)
- Import contacts as CSV is broken
[#931](https://github.com/the-djmaze/snappymail/pull/931)
### Removed
- Blockquote height calculator for
[#902](https://github.com/the-djmaze/snappymail/pull/902)
## 2.25.4 2023-02-06 ## 2.25.4 2023-02-06
## 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 | 415.987 | |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.561 | |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.740 |139.456 | 64.135 |110.485 | 54.920 | |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.774 |358.761 | 98.968 |299.485 | 86.070 | |TOTAL user |1.217.635 | 283.351 |358.761 | 98.712 |299.485 | 85.942 |
|TOTAL user+sieve |1.217.635 | 326.340 |358.761 |109.395 |299.485 | 95.483 | |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 | 82.190 | 46.946 | 17.063 | 14.669 | |app.css | 340.331 | 82.134 | 46.946 | 17.067 | 14.688 |
|app.min.css | 274.947 | 66.363 | 39.647 | 15.092 | 13.246 | |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.535 | | 6.980 | 6.091 | |admin.css | | 30.554 | | 6.984 | 6.085 |
|admin.min.css | | 24.733 | | 6.336 | 5.587 | |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

@ -251,7 +251,7 @@ export class AppUser extends AbstractApp {
setTimeout(() => { setTimeout(() => {
const left = elementById('rl-left'), const left = elementById('rl-left'),
fToggle = () => fToggle = () =>
setLayoutResizer(left, null, ClientSideKeyNameFolderListSize, setLayoutResizer(left, ClientSideKeyNameFolderListSize,
(ThemeStore.isMobile() || leftPanelDisabled()) ? 0 : 'Width'); (ThemeStore.isMobile() || leftPanelDisabled()) ? 0 : 'Width');
if (left) { if (left) {
fToggle(); fToggle();

View file

@ -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'),
@ -196,37 +182,24 @@ showMessageComposer = (params = []) =>
rl.app.showMessageComposer(params); rl.app.showMessageComposer(params);
}, },
setLayoutResizer = (source, target, sClientSideKeyName, mode) => setLayoutResizer = (source, sClientSideKeyName, mode) =>
{ {
if (source.layoutResizer && source.layoutResizer.mode != mode) { if (source.layoutResizer && source.layoutResizer.mode != mode) {
target?.removeAttribute('style');
source.removeAttribute('style'); source.removeAttribute('style');
} }
source.observer?.disconnect(); source.observer?.disconnect();
// source.classList.toggle('resizable', mode); // source.classList.toggle('resizable', mode);
if (mode) { if (mode) {
const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode), const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode);
setTargetPos = mode => {
let value;
if ('Width' == mode) {
value = source.offsetWidth;
target && (target.style.left = value + 'px');
} else {
value = source.offsetHeight;
target && (target.style.top = value + 'px');
}
return value;
};
if (length) { if (length) {
source.style[mode.toLowerCase()] = length + 'px'; source.style[mode.toLowerCase()] = length + 'px';
setTargetPos(mode);
} }
if (!source.layoutResizer) { if (!source.layoutResizer) {
const resizer = createElement('div', {'class':'resizer'}), const resizer = createElement('div', {'class':'resizer'}),
save = (data => Remote.saveSettings(0, data)).debounce(500), save = (data => Remote.saveSettings(0, data)).debounce(500),
size = {}, size = {},
store = () => { store = () => {
const value = setTargetPos(resizer.mode), const value = ('Width' == resizer.mode) ? source.offsetWidth : source.offsetHeight,
prop = resizer.key + resizer.mode; prop = resizer.key + resizer.mode;
(value == Local.get(prop)) || Local.set(prop, value); (value == Local.get(prop)) || Local.set(prop, value);
(value == SettingsGet('Resizer' + prop)) || save({['Resizer' + prop]: value}); (value == SettingsGet('Resizer' + prop)) || save({['Resizer' + prop]: value});
@ -285,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',{
id:id,
hidden:'',
class:'b-text-part'
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '') + (oMessage.pgpSigned() ? ' openpgp-signed' : '')
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '') + (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
+ '">' });
+ '</div>');
oMessage.body = body; oMessage.body = body;
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) { if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
oMessage.viewPlain(); oMessage.viewPlain();
@ -338,8 +300,8 @@ 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);
@ -349,32 +311,6 @@ populateMessageBody = (oMessage, popup) => {
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);
}
}
}
}
} }
} }
popup || MessageUserStore.loading(false); popup || MessageUserStore.loading(false);

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,36 +153,20 @@ 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) {
@ -194,7 +176,7 @@ const
} }
if (vmScreen?.__started) { if (vmScreen?.__started) {
isSameScreen = currentScreen && vmScreen === currentScreen; let isSameScreen = currentScreen && vmScreen === currentScreen;
if (!vmScreen.__builded) { if (!vmScreen.__builded) {
vmScreen.__builded = true; vmScreen.__builded = true;
@ -231,7 +213,6 @@ const
}, 1); }, 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,157 +18,24 @@ 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 = {
address = []; type: 'text',
} else { value: ''
address.push(token); },
} escaped = false,
}); address = [],
addresses = [];
address.length && addresses.push(address);
addresses.forEach(address => {
address = _handleAddress(address);
if (address.length) {
parsedAddresses = parsedAddresses.concat(address);
}
});
return parsedAddresses;
}
/**
* Converts tokens for a single address into an address object
*
* @param {Array} tokens Tokens object
* @return {Object} Address object
*/
function _handleAddress(tokens) {
var isGroup = false;
var state = 'text';
var address = void 0;
var addresses = [];
var data = {
address: [],
comment: [],
group: [],
text: []
};
// Filter out <addresses>, (comments) and regular text
tokens.forEach(token => {
if (token.type === 'operator') {
switch (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 (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
if (isGroup) {
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
data.text = data.text.join(' ');
addresses.push({
name: data.text || address && address.name,
group: data.group.length ? addressparser(data.group.join(',')) : []
});
} else {
// If no address was found, try to detect one from regular text
if (!data.address.length && data.text.length) {
var i = data.text.length;
while (i--) {
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
data.address = data.text.splice(i, 1);
break;
}
}
// still no address
if (!data.address.length) {
i = data.text.length;
while (i--) {
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, address => {
if (!data.address.length) {
data.address = [address.trim()];
return '';
}
return address.trim();
});
if (data.address.length) {
break;
}
}
}
}
// If there's still is no text but a comment exixts, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
// Keep only the first address occurence, push others to regular text
if (data.address.length > 1) {
data.text = data.text.concat(data.address.splice(1));
}
// Join values with spaces
data.text = data.text.join(' ');
data.address = data.address.join(' ');
if (!data.address && isGroup) {
return [];
}
address = {
address: data.address || data.text || '',
name: data.text || data.address || ''
};
if (address.address === address.name) {
if ((address.address || '').match(/@/)) {
address.name = '';
} else {
address.address = '';
}
}
addresses.push(address);
}
return addresses;
}
const
/* /*
* Operator tokens and which tokens are expected to end the sequence * Operator tokens and which tokens are expected to end the sequence
*/ */
var OPERATORS = { OPERATORS = {
'"': '"', '"': '"',
'(': ')', '(': ')',
'<': '>', '<': '>',
@ -182,91 +49,169 @@ var OPERATORS = {
// comma in their UI, it makes sense to treat them the same as a comma // comma in their UI, it makes sense to treat them the same as a comma
// when used outside of a group. // when used outside of a group.
';': '' ';': ''
}; },
pushToken = token => {
class Tokenizer token.value = (token.value || '').toString().trim();
{ token.value.length && address.push(token);
constructor(str) { node = {
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', type: 'text',
value: '' value: ''
},
escaped = false;
},
pushAddress = () => {
if (address.length) {
address = _handleAddress(address);
if (address.length) {
addresses = addresses.concat(address);
}
}
address = [];
}; };
this.list.push(this.node);
[...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 {
node.value += chr;
escaped = !escaped && '\\' === chr;
}
});
pushToken(node);
pushAddress();
return addresses;
// return addresses.map(item => (item.name || item.email) ? new EmailModel(item.email, item.name) : null).filter(v => v);
} }
if (this.escaped && chr !== '\\') { /**
this.node.value += '\\'; * Converts tokens for a single address into an address object
*
* @param {Array} tokens Tokens object
* @return {Object} Address object
*/
function _handleAddress(tokens) {
let
isGroup = false,
address = {},
addresses = [],
data = {
email: [],
comment: [],
group: [],
text: []
};
tokens.forEach(token => {
isGroup = isGroup || 'group' === token.type;
data[token.type].push(token.value);
});
// If there is no text but a comment, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
} }
this.node.value += chr; if (isGroup) {
this.escaped = false; // http://tools.ietf.org/html/rfc2822#appendix-A.1.3
/*
addresses.push({
email: '',
name: data.text.join(' ').trim(),
group: addressparser(data.group.join(','))
// ,comment: data.comment.join(' ').trim()
});
*/
addresses = addresses.concat(addressparser(data.group.join(',')));
} else {
// If no address was found, try to detect one from regular text
if (!data.email.length && data.text.length) {
var i = data.text.length;
while (i--) {
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
data.email = data.text.splice(i, 1);
break;
} }
} }
// still no address
if (!data.email.length) {
i = data.text.length;
while (i--) {
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, address => {
if (!data.email.length) {
data.email = [address.trim()];
return '';
}
return address.trim();
});
if (data.email.length) {
break;
}
}
}
}
// If there's still no text but a comment exists, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
// Keep only the first address occurence, push others to regular text
if (data.email.length > 1) {
data.text = data.text.concat(data.email.splice(1));
}
address = {
// Join values with spaces
email: data.email.join(' ').trim(),
name: data.text.join(' ').trim()
// ,comment: data.comment.join(' ').trim()
};
if (address.email === address.name) {
if (address.email.includes('@')) {
address.name = '';
} else {
address.email = '';
}
}
// address.email = address.email.replace(/^[<]+(.*)[>]+$/g, '$1');
addresses.push(address);
}
return addresses;
}
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,14 +381,12 @@ 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(message, (key, value) => { forEachObjectEntry(this, (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)) {
@ -391,30 +394,37 @@ export class MessageModel extends AbstractModel {
} }
}); });
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);
if (this.from[0]?.email.match(regex)) { if (this.from[0]?.email.match(regex)) {
regex = 0; regex = null;
} }
} }
} }
let hasImages = false, let hasImages = false,
isValid = src => { isValid = src => {
if (!regex || src.match(regex)) { if (null == regex || (regex && src.match(regex))) {
return true; return true;
} }
hasImages = true; hasImages = true;
@ -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

@ -1,11 +1,9 @@
import { Scope } from 'Common/Enums'; import { Scope } from 'Common/Enums';
import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser'; import { doc, createElement, leftPanelDisabled, Settings } from 'Common/Globals';
import { doc, createElement, leftPanelDisabled, Settings, elementById } from 'Common/Globals';
import { pString, pInt } from 'Common/Utils'; import { pString, pInt } from 'Common/Utils';
import { setLayoutResizer, moveAction } from 'Common/UtilsUser'; import { moveAction } from 'Common/UtilsUser';
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache'; import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
import { i18n, initOnStartOrLangChange } from 'Common/Translator'; import { i18n, initOnStartOrLangChange } from 'Common/Translator';
import { SettingsUserStore } from 'Stores/User/Settings';
import { AppUserStore } from 'Stores/User/App'; import { AppUserStore } from 'Stores/User/App';
import { AccountUserStore } from 'Stores/User/Account'; import { AccountUserStore } from 'Stores/User/Account';
@ -113,24 +111,6 @@ export class MailBoxUserScreen extends AbstractScreen {
* @returns {void} * @returns {void}
*/ */
onBuild() { onBuild() {
setTimeout(() => {
// initMailboxLayoutResizer
const top = elementById('V-MailMessageList'),
bottom = elementById('V-MailMessageView'),
fToggle = () => {
let layout = SettingsUserStore.layout();
setLayoutResizer(top, bottom, ClientSideKeyNameMessageListSize,
(ThemeStore.isMobile() || Layout.NoPreview === layout)
? 0
: (Layout.SidePreview === layout ? 'Width' : 'Height')
);
};
if (top && bottom) {
fToggle();
addEventListener('rl-layout', fToggle);
}
}, 1);
doc.addEventListener('click', event => doc.addEventListener('click', event =>
event.target.closest('#rl-right') && moveAction(false) event.target.closest('#rl-right') && moveAction(false)
); );

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,11 +14,13 @@
z-index: 0; z-index: 0;
} }
#rl-right { #rl-right:not([hidden]) {
display: flex;
flex-grow: 1; flex-grow: 1;
max-width: 100vw; width: 20%;
position: relative; }
z-index: 1; .rl-bottom-preview-pane #rl-right {
flex-direction: column;
} }
/* /*
@ -58,82 +60,28 @@ html:not(.rl-left-panel-disabled) #rl-left {
} }
#rl-left > .resizer, #rl-left > .resizer,
.rl-side-preview-pane #V-MailMessageList > .resizer { .rl-side-preview-pane #V-MailMessageList .resizer {
cursor: ew-resize; cursor: ew-resize;
height: 100%; cursor: col-resize;
right: 0; right: 0;
top: 0; top: 0;
bottom: 0;
width: 5px; width: 5px;
} }
.rl-bottom-preview-pane #V-MailMessageList .resizer {
.rl-side-preview-pane #V-MailMessageList {
resize: horizontal; /*overflow: auto;*/
min-width: 320px;
max-width: 60%;
}
.rl-bottom-preview-pane #V-MailMessageList {
resize: vertical; /*overflow: auto;*/
min-height: 200px;
max-height: 60%;
}
.rl-bottom-preview-pane #V-MailMessageList > .resizer {
cursor: ns-resize; cursor: ns-resize;
height: 5px; cursor: row-resize;
left: 0;
bottom: 0; bottom: 0;
width: 100%; left: 0;
right: 0;
height: 5px;
} }
html:not(.rl-left-panel-disabled) #rl-left > .resizer, html:not(.rl-left-panel-disabled) #rl-left > .resizer,
#V-MailMessageList > .resizer { #V-MailMessageList .resizer {
display: block; display: block;
} }
#V-MailMessageList {
position: relative;
height: 100%;
width: 50%;
}
#V-MailMessageView {
position: absolute;
top: 0;
bottom: 13px;
right: 0;
left: 0;
.b-message-view-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, .4);
z-index: 10;
text-align: center;
.backdrop-message {
position: relative;
display: inline-block;
color: white;
font-size: 20px;
line-height: 30px;
background: rgba(0, 0, 0, .6);
padding: 15px;
border-radius: var(--border-radius, 5px);
text-shadow: 0 1px 1px #000;
transform: translateY(-50%);
top: 50%;
}
}
}
html.rl-side-preview-pane {
#V-MailMessageView {
left: 50%;
}
}
#top-system-dropdown-id::after, #top-system-dropdown-id::after,
#button-add-prop-dropdown-id::after { #button-add-prop-dropdown-id::after {
content: '▼'; content: '▼';
@ -201,16 +149,6 @@ html.rl-side-preview-pane {
#rl-left { #rl-left {
width: @rlLeftWidth + 20; width: @rlLeftWidth + 20;
} }
#V-MailMessageList {
width: 40%;
}
html.rl-side-preview-pane {
#V-MailMessageView {
left: 40%;
}
}
} }
/* mobile and tablet */ /* mobile and tablet */
@ -253,17 +191,6 @@ html.rl-left-panel-disabled {
} }
} }
html.rl-no-preview-pane {
#V-MailMessageList {
position: absolute;
left: 0;
right: 0;
width: auto;
}
}
html.rl-mobile #rl-left > .resizer, html.rl-mobile #rl-left > .resizer,
html.rl-no-preview-pane #rl-right .resizer { html.rl-no-preview-pane #rl-right .resizer {
display: none !important; display: none !important;
@ -278,28 +205,13 @@ 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;
} }
} }
html.rl-bottom-preview-pane {
#V-MailMessageList {
height: 300px;
position: absolute;
left: 0;
right: 0;
width: auto;
}
#V-MailMessageView {
top: 300px;
}
}
html.rl-mobile .hide-mobile, html.rl-mobile .hide-mobile,
html:not(.rl-mobile) .show-mobile { html:not(.rl-mobile) .show-mobile {
display: none !important; display: none !important;

View file

@ -5,6 +5,31 @@ html.rl-mobile, html.rl-no-preview-pane {
} }
} }
#V-MailMessageList {
position: relative;
}
.rl-side-preview-pane #V-MailMessageList {
height: 100%;
}
.rl-no-preview-pane #V-MailMessageList {
height: 100%;
width: 100%;
}
.rl-side-preview-pane #V-MailMessageList .messageList {
max-width: 50vw;
min-width: 320px;
overflow: auto;
resize: horizontal;
width: 35vw;
}
.rl-bottom-preview-pane #V-MailMessageList .messageList {
height: 35vh;
max-height: 50vh;
min-height: 200px;
overflow: auto;
resize: vertical;
}
#V-MailMessageList.focused .messageList { #V-MailMessageList.focused .messageList {
border-color: #9d9d9d; border-color: #9d9d9d;
box-shadow: var(--smMainShadow); box-shadow: var(--smMainShadow);

View file

@ -1,4 +1,35 @@
#V-MailMessageView {
flex-grow: 1;
overflow: auto;
padding-bottom: @rlLowMargin;
.b-message-view-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, .4);
z-index: 10;
text-align: center;
.backdrop-message {
position: relative;
display: inline-block;
color: white;
font-size: 20px;
line-height: 30px;
background: rgba(0, 0, 0, .6);
padding: 15px;
border-radius: var(--border-radius, 5px);
text-shadow: 0 1px 1px #000;
transform: translateY(-50%);
top: 50%;
}
}
}
html.rl-no-preview-pane { html.rl-no-preview-pane {
#rl-right:not(.message-selected) #V-MailMessageView { #rl-right:not(.message-selected) #V-MailMessageView {
display: none; display: none;
@ -317,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

@ -48,6 +48,9 @@ import { ComposePopupView } from 'View/Popup/Compose';
import { MessageModel } from 'Model/Message'; import { MessageModel } from 'Model/Message';
import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser';
import { setLayoutResizer } from 'Common/UtilsUser';
const const
canBeMovedHelper = () => MessagelistUserStore.hasCheckedOrSelected(), canBeMovedHelper = () => MessagelistUserStore.hasCheckedOrSelected(),
@ -159,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);
@ -232,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);
} }
@ -564,6 +568,23 @@ export class MailMessageList extends AbstractViewRight {
const b_content = dom.querySelector('.b-content'), const b_content = dom.querySelector('.b-content'),
eqs = (ev, s) => ev.target.closestWithin(s, dom); eqs = (ev, s) => ev.target.closestWithin(s, dom);
setTimeout(() => {
// initMailboxLayoutResizer
const top = dom.querySelector('.messageList'),
fToggle = () => {
let layout = SettingsUserStore.layout();
setLayoutResizer(top, ClientSideKeyNameMessageListSize,
(ThemeStore.isMobile() || Layout.NoPreview === layout)
? 0
: (Layout.SidePreview === layout ? 'Width' : 'Height')
);
};
if (top) {
fToggle();
addEventListener('rl-layout', fToggle);
}
}, 1);
this.selector.init(b_content, Scope.MessageList); this.selector.init(b_content, Scope.MessageList);
addEventsListeners(dom, { addEventsListeners(dom, {

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.4</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.4 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.4</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.4" ); 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.4", "version": "2.26.1",
"homepage": "https://snappymail.eu", "homepage": "https://snappymail.eu",
"author": { "author": {
"name": "DJ Maze", "name": "DJ Maze",

View file

@ -133,13 +133,17 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt
$oParams->iLimit = 999; // Is the max $oParams->iLimit = 999; // Is the max
$oMessageList = $this->MailClient()->MessageList($oParams); $oMessageList = $this->MailClient()->MessageList($oParams);
foreach ($oMessageList as $oMessage) { foreach ($oMessageList as $oMessage) {
try {
if ($rCsv) { if ($rCsv) {
$oContact = $this->MessageAsContact($oMessage); $oContact = $this->MessageAsContact($oMessage);
\RainLoop\Providers\AddressBook\Utils::VCardToCsv($rCsv, $oContact, $bCsvHeader); \RainLoop\Providers\AddressBook\Utils::VCardToCsv($rCsv, $oContact->vCard, $bCsvHeader);
$bCsvHeader = false; $bCsvHeader = false;
} else if ($xCard = $this->fetchXCardFromMessage($oMessage)) { } else if ($xCard = $this->fetchXCardFromMessage($oMessage)) {
echo $xCard->serialize(); echo $xCard->serialize();
} }
} catch (\Throwable $oExc) {
$this->oLogger && $this->oLogger->WriteException($oExc);
}
} }
} }
catch (\Throwable $oException) catch (\Throwable $oException)
@ -156,7 +160,7 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt
return false; return false;
} }
$id = $oContact->id; $id = \intval($oContact->id);
$sUID = ''; $sUID = '';
$oVCard = $oContact->vCard; $oVCard = $oContact->vCard;

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

@ -4,8 +4,8 @@ class LoginOverridePlugin extends \RainLoop\Plugins\AbstractPlugin
{ {
const const
NAME = 'Login Override', NAME = 'Login Override',
VERSION = '2.1', VERSION = '2.2',
RELEASE = '2023-02-01', RELEASE = '2023-02-08',
REQUIRED = '2.25.3', REQUIRED = '2.25.3',
CATEGORY = 'Filters', CATEGORY = 'Filters',
DESCRIPTION = 'Override IMAP/SMTP login credentials for specific users.'; DESCRIPTION = 'Override IMAP/SMTP login credentials for specific users.';
@ -22,7 +22,7 @@ class LoginOverridePlugin extends \RainLoop\Plugins\AbstractPlugin
$sMapping = \trim($this->Config()->Get('plugin', 'email_mapping', '')); $sMapping = \trim($this->Config()->Get('plugin', 'email_mapping', ''));
if (!empty($sMapping)) { if (!empty($sMapping)) {
$aList = \preg_split('/\\R/', $sMapping); $aList = \preg_split('/\\R/', $sMapping);
foreach ($aList as $line) { foreach ($aList as $sLine) {
$aData = \explode(':', $sLine, 2); $aData = \explode(':', $sLine, 2);
if (!empty($aData[1]) && $sEmail === \trim($aData[0])) { if (!empty($aData[1]) && $sEmail === \trim($aData[0])) {
$sEmail = \trim($aData[1]); $sEmail = \trim($aData[1]);

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
{ {
$sInReplyTo = \trim($sInReplyTo);
if (\strlen($sInReplyTo)) {
$this->aHeadersValue[Enumerations\Header::IN_REPLY_TO] = $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
{ {
if ($oEmails->count()) {
$this->aHeadersValue[Enumerations\Header::TO_] = $oEmails; $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
{ {
if ($oEmails->count()) {
$this->aHeadersValue[Enumerations\Header::REPLY_TO] = $oEmails; $this->aHeadersValue[Enumerations\Header::REPLY_TO] = $oEmails;
}
return $this; return $this;
} }
public function SetCc(EmailCollection $oEmails) : self public function SetCc(EmailCollection $oEmails) : self
{ {
if ($oEmails->count()) {
$this->aHeadersValue[Enumerations\Header::CC] = $oEmails; $this->aHeadersValue[Enumerations\Header::CC] = $oEmails;
}
return $this; return $this;
} }
public function SetBcc(EmailCollection $oEmails) : self public function SetBcc(EmailCollection $oEmails) : self
{ {
if ($oEmails->count()) {
$this->aHeadersValue[Enumerations\Header::BCC] = $oEmails; $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

@ -133,9 +133,8 @@ trait Contacts
\ini_set('auto_detect_line_endings', '1'); \ini_set('auto_detect_line_endings', '1');
$mData = $this->FilesProvider()->GetFile($oAccount, $sSavedName); $mData = $this->FilesProvider()->GetFile($oAccount, $sSavedName);
if ($mData) { if ($mData) {
$sFileStart = \fread($mData, 20); $sFileStart = \fread($mData, 128);
\rewind($mData); \rewind($mData);
if (false !== $sFileStart) { if (false !== $sFileStart) {
$sFileStart = \trim($sFileStart); $sFileStart = \trim($sFileStart);
if (false !== \strpos($sFileStart, 'BEGIN:VCARD')) { if (false !== \strpos($sFileStart, 'BEGIN:VCARD')) {
@ -255,64 +254,45 @@ trait Contacts
private function importContactsFromVcfFile(\RainLoop\Model\Account $oAccount, /*resource*/ $rFile): int private function importContactsFromVcfFile(\RainLoop\Model\Account $oAccount, /*resource*/ $rFile): int
{ {
$iCount = 0; $iCount = 0;
if ($oAccount && \is_resource($rFile)) {
$oAddressBookProvider = $this->AddressBookProvider($oAccount); $oAddressBookProvider = $this->AddressBookProvider($oAccount);
if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) { if (\is_resource($rFile) && $oAddressBookProvider && $oAddressBookProvider->IsActive()) {
$sFile = \stream_get_contents($rFile); try
if (\is_resource($rFile)) { {
\fclose($rFile);
}
if (is_string($sFile) && 5 < \strlen($sFile)) {
$this->Logger()->Write('Import contacts from vcf'); $this->Logger()->Write('Import contacts from vcf');
$iCount = $oAddressBookProvider->ImportVcfFile($sFile); foreach (\RainLoop\Providers\AddressBook\Utils::VcfStreamToContacts($rFile) as $oContact) {
if ($oAddressBookProvider->ContactSave($oContact)) {
++$iCount;
} }
} }
} }
catch (\Throwable $oExc)
{
$this->Logger()->WriteException($oExc);
}
}
return $iCount; return $iCount;
} }
private function importContactsFromCsvFile(\RainLoop\Model\Account $oAccount, /*resource*/ $rFile, string $sFileStart): int private function importContactsFromCsvFile(\RainLoop\Model\Account $oAccount, /*resource*/ $rFile, string $sFileStart): int
{ {
$iCount = 0; $iCount = 0;
$aHeaders = null;
$aData = array();
if ($oAccount && \is_resource($rFile)) {
$oAddressBookProvider = $this->AddressBookProvider($oAccount); $oAddressBookProvider = $this->AddressBookProvider($oAccount);
if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) { if (\is_resource($rFile) && $oAddressBookProvider && $oAddressBookProvider->IsActive()) {
try
{
$this->Logger()->Write('Import contacts from csv');
$sDelimiter = ((int)\strpos($sFileStart, ',') > (int)\strpos($sFileStart, ';')) ? ',' : ';'; $sDelimiter = ((int)\strpos($sFileStart, ',') > (int)\strpos($sFileStart, ';')) ? ',' : ';';
foreach (\RainLoop\Providers\AddressBook\Utils::CsvStreamToContacts($rFile, $sDelimiter) as $oContact) {
\setlocale(LC_CTYPE, 'en_US.UTF-8'); if ($oAddressBookProvider->ContactSave($oContact)) {
while (false !== ($mRow = \fgetcsv($rFile, 5000, $sDelimiter, '"'))) { ++$iCount;
if (null === $aHeaders) {
if (3 >= \count($mRow)) {
return 0;
}
$aHeaders = $mRow;
foreach ($aHeaders as $iIndex => $sHeaderValue) {
$aHeaders[$iIndex] = \MailSo\Base\Utils::Utf8Clear($sHeaderValue);
}
} else {
$aNewItem = array();
foreach ($aHeaders as $iIndex => $sHeaderValue) {
$aNewItem[$sHeaderValue] = isset($mRow[$iIndex]) ? $mRow[$iIndex] : '';
}
$aData[] = $aNewItem;
}
}
if (\count($aData)) {
$this->oLogger->Write('Import contacts from csv');
$iCount = $oAddressBookProvider->ImportCsvArray($aData);
} }
} }
} }
catch (\Throwable $oExc)
{
$this->Logger()->WriteException($oExc);
}
}
return $iCount; return $iCount;
} }

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

@ -82,37 +82,4 @@ class AddressBook extends AbstractProvider
{ {
return $this->IsActive() ? $this->oDriver->IncFrec($aEmails, $bCreateAuto) : false; return $this->IsActive() ? $this->oDriver->IncFrec($aEmails, $bCreateAuto) : false;
} }
public function ImportCsvArray(array $aCsvData) : int
{
$iCount = 0;
if ($this->IsActive()) {
foreach (AddressBook\Utils::CsvArrayToContacts($aCsvData) as $oContact) {
if ($this->ContactSave($oContact)) {
++$iCount;
}
}
}
return $iCount;
}
public function ImportVcfFile(string $sVcfData) : int
{
$iCount = 0;
if ($this->IsActive()) {
try
{
foreach (AddressBook\Utils::VcfFileToContacts($sVcfData) as $oContact) {
if ($this->ContactSave($oContact)) {
++$iCount;
}
}
}
catch (\Throwable $oExc)
{
$this->Logger()->WriteException($oExc);
}
}
return $iCount;
}
} }

View file

@ -52,11 +52,17 @@ class PdoAddressBook
$sDsn = 'sqlite:' . APP_PRIVATE_DATA . 'AddressBook.sqlite'; $sDsn = 'sqlite:' . APP_PRIVATE_DATA . 'AddressBook.sqlite';
/* /*
// TODO: use local db? // TODO: use local db?
$oAccount = \RainLoop\Api::Actions()->getMainAccountFromToken(false);
if ($oAccount) {
$homedir = \RainLoop\Api::Actions()->StorageProvider()->GenerateFilePath( $homedir = \RainLoop\Api::Actions()->StorageProvider()->GenerateFilePath(
$oAccount, $oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::ROOT \RainLoop\Providers\Storage\Enumerations\StorageType::ROOT
); );
$sDsn = 'sqlite:' . $homedir . '/AddressBook.sqlite'; if (!\is_file($homedir . 'AddressBook.sqlite') && \is_file(APP_PRIVATE_DATA . '/AddressBook.sqlite')) {
\copy(APP_PRIVATE_DATA . '/AddressBook.sqlite', $homedir . 'AddressBook.sqlite');
}
$sDsn = 'sqlite:' . $homedir . 'AddressBook.sqlite';
}
*/ */
} else { } else {
$sDsn = \trim($oConfig->Get('contacts', 'pdo_dsn', '')); $sDsn = \trim($oConfig->Get('contacts', 'pdo_dsn', ''));
@ -101,9 +107,12 @@ class PdoAddressBook
private function prepareDatabaseSyncData() : array private function prepareDatabaseSyncData() : array
{ {
$aResult = array(); $aResult = array();
$oStmt = $this->prepareAndExecute('SELECT id_contact, id_contact_str, changed, deleted, etag FROM rainloop_ab_contacts WHERE id_user = :id_user', array( $oStmt = $this->prepareAndExecute('SELECT id_contact, id_contact_str, changed, deleted, etag
':id_user' => array($this->iUserID, \PDO::PARAM_INT) FROM rainloop_ab_contacts
)); WHERE id_user = :id_user
ORDER BY deleted DESC',
array(':id_user' => array($this->iUserID, \PDO::PARAM_INT))
);
if ($oStmt) { if ($oStmt) {
$aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC);
@ -308,6 +317,7 @@ class PdoAddressBook
public function Export(string $sType = 'vcf') : bool public function Export(string $sType = 'vcf') : bool
{ {
if (1 > $this->iUserID) { if (1 > $this->iUserID) {
\SnappyMail\Log::warning('PdoAddressBook', 'Export() invalid $iUserID');
return false; return false;
} }
@ -317,17 +327,22 @@ class PdoAddressBook
$aDatabaseSyncData = $this->prepareDatabaseSyncData(); $aDatabaseSyncData = $this->prepareDatabaseSyncData();
if (\count($aDatabaseSyncData)) { if (\count($aDatabaseSyncData)) {
foreach ($aDatabaseSyncData as $mData) { foreach ($aDatabaseSyncData as $mData) {
if ($mData && isset($mData['id_contact'], $mData['deleted']) && !$mData['deleted']) { try {
// if ($mData && isset($mData['id_contact'], $mData['deleted']) && !$mData['deleted']) {
if ($mData && !empty($mData['id_contact'])) {
$oContact = $this->GetContactByID($mData['id_contact']); $oContact = $this->GetContactByID($mData['id_contact']);
if ($oContact) { if ($oContact) {
if ($rCsv) { if ($rCsv) {
Utils::VCardToCsv($rCsv, $oContact, $bCsvHeader); Utils::VCardToCsv($rCsv, $oContact->vCard, $bCsvHeader);
$bCsvHeader = false; $bCsvHeader = false;
} else { } else {
echo $oContact->vCard->serialize(); echo $oContact->vCard->serialize();
} }
} }
} }
} catch (\Throwable $oExc) {
$this->oLogger && $this->oLogger->WriteException($oExc);
}
} }
} }
@ -337,6 +352,7 @@ class PdoAddressBook
public function ContactSave(Contact $oContact) : bool public function ContactSave(Contact $oContact) : bool
{ {
if (1 > $this->iUserID) { if (1 > $this->iUserID) {
\SnappyMail\Log::warning('PdoAddressBook', 'ContactSave() invalid $iUserID');
return false; return false;
} }
@ -436,6 +452,7 @@ class PdoAddressBook
public function DeleteContacts(array $aContactIds) : bool public function DeleteContacts(array $aContactIds) : bool
{ {
if (1 > $this->iUserID) { if (1 > $this->iUserID) {
\SnappyMail\Log::warning('PdoAddressBook', 'DeleteContacts() invalid $iUserID');
return false; return false;
} }
@ -744,7 +761,7 @@ class PdoAddressBook
p.prop_value as jcard p.prop_value as jcard
FROM rainloop_ab_contacts AS c FROM rainloop_ab_contacts AS c
LEFT JOIN rainloop_ab_properties AS p ON (p.id_contact = c.id_contact AND p.prop_type = :prop_type) LEFT JOIN rainloop_ab_properties AS p ON (p.id_contact = c.id_contact AND p.prop_type = :prop_type)
WHERE c.deleted = 0 AND c.id_user = :id_user'; WHERE c.id_user = :id_user AND c.deleted = 0';
$aParams = array( $aParams = array(
':id_user' => array($this->iUserID, \PDO::PARAM_INT), ':id_user' => array($this->iUserID, \PDO::PARAM_INT),

View file

@ -2,6 +2,8 @@
namespace RainLoop\Providers\AddressBook; namespace RainLoop\Providers\AddressBook;
use Sabre\VObject\Component\VCard;
class Utils class Utils
{ {
private static $aMap = array( private static $aMap = array(
@ -22,20 +24,29 @@ class Utils
'namesuffix' => 4, 'namesuffix' => 4,
'shortname' => 'NICKNAME', 'shortname' => 'NICKNAME',
'nickname' => 'NICKNAME', 'nickname' => 'NICKNAME',
'birthday' => 'BDAY',
'mobile' => array('TEL', 'CELL'),
'mobilephone' => array('TEL', 'CELL'),
'businessemail' => array('EMAIL', 'WORK'),
'businessemail2' => array('EMAIL', 'WORK'),
'businessemail3' => array('EMAIL', 'WORK'),
'businessphone' => array('TEL', 'WORK'), 'businessphone' => array('TEL', 'WORK'),
'businessphone2' => array('TEL', 'WORK'), 'businessphone2' => array('TEL', 'WORK'),
'businessphone3' => array('TEL', 'WORK'), 'businessphone3' => array('TEL', 'WORK'),
'businessmobile' => array('TEL', 'WORK,CELL'),
'businessmobilephone' => array('TEL', 'WORK,CELL'),
'businessweb' => array('URL', 'WORK'),
'businesswebpage' => array('URL', 'WORK'),
'businesswebsite' => array('URL', 'WORK'),
'companyphone' => array('TEL', 'WORK'), 'companyphone' => array('TEL', 'WORK'),
'companymainphone' => array('TEL', 'WORK'), 'companymainphone' => array('TEL', 'WORK'),
'primaryphone' => array('TEL', 'PREF,HOME'),
'homephone' => array('TEL', 'HOME'), 'homephone' => array('TEL', 'HOME'),
'homephone2' => array('TEL', 'HOME'), 'homephone2' => array('TEL', 'HOME'),
'homephone3' => array('TEL', 'HOME'), 'homephone3' => array('TEL', 'HOME'),
'mobile' => array('TEL', 'CELL'),
'mobilephone' => array('TEL', 'CELL'),
'businessmobile' => array('TEL', 'WORK,CELL'),
'businessmobilephone' => array('TEL', 'WORK,CELL'),
'otherphone' => 'TEL',
'primaryphone' => array('TEL', 'PREF,HOME'),
'email' => array('EMAIL', 'HOME'), 'email' => array('EMAIL', 'HOME'),
'email2' => array('EMAIL', 'HOME'), 'email2' => array('EMAIL', 'HOME'),
'email3' => array('EMAIL', 'HOME'), 'email3' => array('EMAIL', 'HOME'),
@ -48,48 +59,69 @@ class Utils
'emailaddress' => array('EMAIL', 'HOME'), 'emailaddress' => array('EMAIL', 'HOME'),
'email2address' => array('EMAIL', 'HOME'), 'email2address' => array('EMAIL', 'HOME'),
'email3address' => array('EMAIL', 'HOME'), 'email3address' => array('EMAIL', 'HOME'),
'otheremail' => 'EMAIL',
'businessemail' => array('EMAIL', 'WORK'),
'businessemail2' => array('EMAIL', 'WORK'),
'businessemail3' => array('EMAIL', 'WORK'),
'personalemail' => array('EMAIL', 'HOME'), 'personalemail' => array('EMAIL', 'HOME'),
'personalemail2' => array('EMAIL', 'HOME'), 'personalemail2' => array('EMAIL', 'HOME'),
'personalemail3' => array('EMAIL', 'HOME'), 'personalemail3' => array('EMAIL', 'HOME'),
'personalwebsite' => array('URL', 'HOME'),
'otheremail' => 'EMAIL',
'otherphone' => 'TEL',
'notes' => 'NOTE', 'notes' => 'NOTE',
'web' => 'URL', 'web' => 'URL',
'businessweb' => array('URL', 'WORK'),
'webpage' => 'URL', 'webpage' => 'URL',
'businesswebpage' => array('URL', 'WORK'), 'website' => 'URL'
'website' => 'URL', /*
'businesswebsite' => array('URL', 'WORK'), TODO:
'personalwebsite' => 'URL', 'company' => '',
'birthday' => 'BDAY' 'department' => '',
'jobtitle' => '',
'officelocation' => '',
'homestreet' => '',
'homecity' => '',
'homestate' => '',
'homepostalcode' => '',
'homecountry' => '',
'businessstreet' => '',
'businesscity' => '',
'businessstate' => '',
'businesspostalcode' => '',
'businesscountry' => '',
*/
); );
public static function CsvArrayToContacts(array $aCsvData) : iterable public static function CsvStreamToContacts(/*resource*/ $rFile, string $sDelimiter) : iterable
{ {
foreach ($aCsvData as $aItem) { \setlocale(LC_CTYPE, 'en_US.UTF-8');
$aHeaders = \fgetcsv($rFile, 5000, $sDelimiter, '"');
if (!$aHeaders || 3 >= \count($aHeaders)) {
return;
}
foreach ($aHeaders as $iIndex => $sItemName) {
$sItemName = \MailSo\Base\Utils::Utf8Clear($sItemName);
$sItemName = \strtoupper(\trim(\preg_replace('/[\s\-]+/', '', $sItemName)));
if (!\array_key_exists($sItemName, VCard::$propertyMap)) {
$sItemName = \strtolower($sItemName);
$sItemName = isset(static::$aMap[$sItemName]) ? static::$aMap[$sItemName] : null;
}
$aHeaders[$iIndex] = $sItemName;
}
while (false !== ($mRow = \fgetcsv($rFile, 5000, $sDelimiter, '"'))) {
\MailSo\Base\Utils::ResetTimeLimit(); \MailSo\Base\Utils::ResetTimeLimit();
$iCount = 0; $iCount = 0;
$oVCard = new \Sabre\VObject\Component\VCard; $oVCard = new VCard;
$aName = ['','','','','']; $aName = ['','','','',''];
foreach ($aItem as $sItemName => $sItemValue) { foreach ($mRow as $iIndex => $sItemValue) {
$sItemName = \strtoupper(\trim(\preg_replace('/[\s\-]+/', '', $sItemName))); $sItemName = $aHeaders[$iIndex];
$sItemValue = \trim($sItemValue); $sItemValue = \trim($sItemValue);
if (!empty($sItemName) && !empty($sItemValue)) { if (isset($sItemName) && !empty($sItemValue)) {
if (\array_key_exists($sItemName, \Sabre\VObject\Component\VCard::$propertyMap)) { $mType = \is_array($sItemName) ? $sItemName[0] : $sItemName;
$mData = $sItemName;
} else {
$sItemName = \strtolower($sItemName);
$mData = !empty($sItemName) && isset($aMap[$sItemName]) ? $aMap[$sItemName] : null;
}
if ($mData) {
$mType = \is_array($mData) ? $mData[0] : $mData;
++$iCount; ++$iCount;
if (\is_int($mType)) { if (\is_int($mType)) {
$aName[$mType] = $sItemValue; $aName[$mType] = $sItemValue;
} else if (\is_array($mData)) { } else if (\is_array($sItemName)) {
$oVCard->add($mType, $sItemValue, ['type' => $mData[1]]); $oVCard->add($mType, $sItemValue, ['type' => $sItemName[1]]);
} else if ('FN' === $mType || 'NICKNAME' === $mType) { } else if ('FN' === $mType || 'NICKNAME' === $mType) {
$oVCard->$mType = $sItemValue; $oVCard->$mType = $sItemValue;
} else { } else {
@ -97,7 +129,6 @@ class Utils
} }
} }
} }
}
if ($iCount) { if ($iCount) {
if ('' !== \implode('', $aName)) { if ('' !== \implode('', $aName)) {
$oVCard->N = $aName; $oVCard->N = $aName;
@ -109,7 +140,7 @@ class Utils
} }
} }
public static function VCardToCsv($stream, Classes\Contact $oContact, bool $bWithHeader = false)/* : int|false*/ public static function VCardToCsv($stream, VCard $oVCard, bool $bWithHeader = false)/* : int|false*/
{ {
$aData = array(); $aData = array();
if ($bWithHeader) { if ($bWithHeader) {
@ -124,8 +155,6 @@ class Utils
)); ));
} }
$oVCard = $oContact->vCard;
$aName = isset($oVCard->N) ? $oVCard->N->getParts() : ['','','','','']; $aName = isset($oVCard->N) ? $oVCard->N->getParts() : ['','','','',''];
$adrHome = $oVCard->getByType('ADR', 'HOME'); $adrHome = $oVCard->getByType('ADR', 'HOME');
@ -169,16 +198,12 @@ class Utils
)); ));
} }
public static function VcfFileToContacts(string $sVcfData) : iterable public static function VcfStreamToContacts(/*resource*/ $rFile) : iterable
{ {
$sVcfData = \trim($sVcfData); $oVCardSplitter = new \Sabre\VObject\Splitter\VCard($rFile);
if ("\xef\xbb\xbf" === \substr($sVcfData, 0, 3)) {
$sVcfData = \substr($sVcfData, 3);
}
$oVCardSplitter = new \Sabre\VObject\Splitter\VCard($sVcfData);
if ($oVCardSplitter) { if ($oVCardSplitter) {
while ($oVCard = $oVCardSplitter->getNext()) { while ($oVCard = $oVCardSplitter->getNext()) {
if ($oVCard instanceof \Sabre\VObject\Component\VCard) { if ($oVCard instanceof VCard) {
\MailSo\Base\Utils::ResetTimeLimit(); \MailSo\Base\Utils::ResetTimeLimit();
$oContact = new Classes\Contact(); $oContact = new Classes\Contact();
$oContact->setVCard($oVCard); $oContact->setVCard($oVCard);

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

@ -236,7 +236,7 @@ abstract class Upgrade
new \RecursiveDirectoryIterator("{$dir}/{$folder}", \FilesystemIterator::SKIP_DOTS), new \RecursiveDirectoryIterator("{$dir}/{$folder}", \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST \RecursiveIteratorIterator::SELF_FIRST
); );
foreach ($items as $item) { foreach ($iterator as $item) {
if ($item->isDir()) { if ($item->isDir()) {
\chmod($item, 0755); \chmod($item, 0755);
} else if ($item->isFile()) { } else if ($item->isFile()) {

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

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