mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-08 22:18:28 +03:00
Merge branch 'the-djmaze:master' into rework-on-mailaddress-handling
This commit is contained in:
commit
aa03e84c79
107 changed files with 972 additions and 1018 deletions
|
|
@ -316,5 +316,5 @@ dev_email = ""
|
|||
dev_password = ""
|
||||
|
||||
[version]
|
||||
current = "2.25.4"
|
||||
current = "2.26.1"
|
||||
saved = "Sun, 18 Dec 2022 22:10:48 +0000"
|
||||
|
|
|
|||
98
CHANGELOG.md
98
CHANGELOG.md
|
|
@ -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
|
||||
|
||||
## Added
|
||||
|
|
|
|||
24
README.md
24
README.md
|
|
@ -141,26 +141,26 @@ RainLoop 1.17 vs SnappyMail
|
|||
|
||||
|js/* |RainLoop |Snappy |
|
||||
|--------------- |--------: |--------: |
|
||||
|admin.js |2.170.153 | 81.589 |
|
||||
|app.js |4.207.787 | 415.987 |
|
||||
|admin.js |2.170.153 | 81.159 |
|
||||
|app.js |4.207.787 | 414.949 |
|
||||
|boot.js | 868.735 | 2.711 |
|
||||
|libs.js | 658.812 | 192.392 |
|
||||
|sieve.js | 0 | 85.597 |
|
||||
|polyfills.js | 334.608 | 0 |
|
||||
|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 |
|
||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||
|admin.min.js | 256.831 | 40.319 | 73.606 | 13.343 | 60.877 | 11.970 |
|
||||
|app.min.js | 515.367 | 191.740 |139.456 | 64.135 |110.485 | 54.920 |
|
||||
|admin.min.js | 256.831 | 40.198 | 73.606 | 13.281 | 60.877 | 11.916 |
|
||||
|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 |
|
||||
|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 |
|
||||
|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+sieve |1.217.635 | 326.340 |358.761 |109.395 |299.485 | 95.483 |
|
||||
|TOTAL admin | 959.099 | 133.353 |292.911 | 48.176 |249.877 | 43.120 |
|
||||
|TOTAL user |1.217.635 | 283.351 |358.761 | 98.712 |299.485 | 85.942 |
|
||||
|TOTAL user+sieve |1.217.635 | 324.917 |358.761 |109.139 |299.485 | 95.355 |
|
||||
|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.
|
||||
|
||||
|
|
@ -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 |
|
||||
|------------ |-------: |------: |------: |------: |--------: |
|
||||
|app.css | 340.331 | 82.190 | 46.946 | 17.063 | 14.669 |
|
||||
|app.min.css | 274.947 | 66.363 | 39.647 | 15.092 | 13.246 |
|
||||
|app.css | 340.331 | 82.134 | 46.946 | 17.067 | 14.688 |
|
||||
|app.min.css | 274.947 | 66.337 | 39.647 | 15.095 | 13.285 |
|
||||
|boot.css | | 1.326 | | 664 | 545 |
|
||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||
|admin.css | | 30.535 | | 6.980 | 6.091 |
|
||||
|admin.min.css | | 24.733 | | 6.336 | 5.587 |
|
||||
|admin.css | | 30.554 | | 6.984 | 6.085 |
|
||||
|admin.min.css | | 24.752 | | 6.339 | 5.606 |
|
||||
|
||||
### PGP
|
||||
RainLoop uses the old OpenPGP.js v2
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ export class AppUser extends AbstractApp {
|
|||
setTimeout(() => {
|
||||
const left = elementById('rl-left'),
|
||||
fToggle = () =>
|
||||
setLayoutResizer(left, null, ClientSideKeyNameFolderListSize,
|
||||
setLayoutResizer(left, ClientSideKeyNameFolderListSize,
|
||||
(ThemeStore.isMobile() || leftPanelDisabled()) ? 0 : 'Width');
|
||||
if (left) {
|
||||
fToggle();
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ folderInformation = (folder, list) => {
|
|||
const oldHash = folderFromCache.hash,
|
||||
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
|
||||
|
||||
// folderFromCache.revivePropertiesFromJson(result);
|
||||
// folderFromCache.revivePropertiesFromJson(result);
|
||||
folderFromCache.expires = Date.now();
|
||||
folderFromCache.uidNext = result.uidNext;
|
||||
folderFromCache.hash = result.hash;
|
||||
|
|
@ -151,9 +151,11 @@ folderInformation = (folder, list) => {
|
|||
if (!oldHash || unreadCountChange || result.hash !== oldHash) {
|
||||
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
|
||||
MessagelistUserStore.reload();
|
||||
/*
|
||||
} else if (getFolderInboxName() === folderFromCache.fullName) {
|
||||
// Remote.messageList(null, {folder: getFolderFromCacheList(getFolderInboxName())}, true);
|
||||
Remote.messageList(null, {folder: getFolderInboxName()}, true);
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ const
|
|||
SettingsUserStore.collapseBlockquotes() &&
|
||||
// tpl.content.querySelectorAll('blockquote').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);
|
||||
el.append(node);
|
||||
});
|
||||
|
|
@ -87,6 +88,86 @@ const
|
|||
});
|
||||
}
|
||||
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
|
||||
|
|
@ -102,7 +183,7 @@ export const
|
|||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
cleanHtml = (html, oAttachments) => {
|
||||
cleanHtml = (html, oAttachments, msgId) => {
|
||||
let aColor;
|
||||
const
|
||||
debug = false, // Config()->Get('debug', 'enable', false);
|
||||
|
|
@ -159,7 +240,7 @@ export const
|
|||
'colspan', 'rowspan', 'headers'
|
||||
],
|
||||
disallowedTags = [
|
||||
'STYLE','SVG','SCRIPT','TITLE','LINK','BASE','META',
|
||||
'SVG','SCRIPT','TITLE','LINK','BASE','META',
|
||||
'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA',
|
||||
'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
|
||||
// Not supported by <template> element
|
||||
|
|
@ -169,7 +250,15 @@ export const
|
|||
'A','B','EM','I','SPAN','STRONG'
|
||||
];
|
||||
|
||||
if (SettingsUserStore.allowStyles()) {
|
||||
allowedAttributes.push('class');
|
||||
} else {
|
||||
msgId = 0;
|
||||
}
|
||||
|
||||
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>'))
|
||||
// Not supported by <template> element
|
||||
// .replace(/<!doctype[^>]*>/gi, '')
|
||||
|
|
@ -198,6 +287,21 @@ export const
|
|||
const name = oElement.tagName,
|
||||
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()
|
||||
if (disallowedTags.includes(name)
|
||||
|| 'none' == oStyle.display
|
||||
|
|
@ -210,29 +314,23 @@ export const
|
|||
oElement.remove();
|
||||
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 = [],
|
||||
className = oElement.className,
|
||||
hasAttribute = name => oElement.hasAttribute(name),
|
||||
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
|
||||
setAttribute = (name, value) => oElement.setAttribute(name, value),
|
||||
delAttribute = name => oElement.removeAttribute(name);
|
||||
|
||||
if ('mail-body' === oElement.className) {
|
||||
if ('mail-body' === className) {
|
||||
forEachObjectEntry(tasks, (name, cb) => {
|
||||
if (hasAttribute(name)) {
|
||||
cb(getAttribute(name), oElement);
|
||||
delAttribute(name);
|
||||
}
|
||||
});
|
||||
} else if (msgId && className) {
|
||||
oElement.className = className.replace(/(^|\s+)/g, '$1msg-');
|
||||
}
|
||||
|
||||
if (oElement.hasAttributes()) {
|
||||
|
|
@ -258,7 +356,7 @@ export const
|
|||
delAttribute('width');
|
||||
}
|
||||
value = oStyle.removeProperty('width');
|
||||
if (value && !oStyle.maxWidth) {
|
||||
if (parseInt(value,10) && !oStyle.maxWidth) {
|
||||
oStyle.maxWidth = value;
|
||||
oStyle.width = '100%';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser'
|
|||
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
|
||||
import { plainToHtml } from 'Common/Html';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { EmailCollectionModel } from 'Model/EmailCollection';
|
||||
import { MessageModel } from 'Model/Message';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
|
|
@ -158,27 +158,13 @@ mailToHelper = mailToUrl => {
|
|||
const
|
||||
email = mailToUrl[0],
|
||||
params = new URLSearchParams(mailToUrl[1]),
|
||||
toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null;
|
||||
to = params.get('to'),
|
||||
toEmailModel = value => EmailCollectionModel.fromString(value);
|
||||
|
||||
showMessageComposer([
|
||||
ComposeType.Empty,
|
||||
null,
|
||||
params.get('to')
|
||||
? 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(to ? email + ',' + to : email),
|
||||
toEmailModel(params.get('cc')),
|
||||
toEmailModel(params.get('bcc')),
|
||||
params.get('subject'),
|
||||
|
|
@ -196,37 +182,24 @@ showMessageComposer = (params = []) =>
|
|||
rl.app.showMessageComposer(params);
|
||||
},
|
||||
|
||||
setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
|
||||
setLayoutResizer = (source, sClientSideKeyName, mode) =>
|
||||
{
|
||||
if (source.layoutResizer && source.layoutResizer.mode != mode) {
|
||||
target?.removeAttribute('style');
|
||||
source.removeAttribute('style');
|
||||
}
|
||||
source.observer?.disconnect();
|
||||
// source.classList.toggle('resizable', mode);
|
||||
if (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;
|
||||
};
|
||||
const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode);
|
||||
if (length) {
|
||||
source.style[mode.toLowerCase()] = length + 'px';
|
||||
setTargetPos(mode);
|
||||
}
|
||||
if (!source.layoutResizer) {
|
||||
const resizer = createElement('div', {'class':'resizer'}),
|
||||
save = (data => Remote.saveSettings(0, data)).debounce(500),
|
||||
size = {},
|
||||
store = () => {
|
||||
const value = setTargetPos(resizer.mode),
|
||||
const value = ('Width' == resizer.mode) ? source.offsetWidth : source.offsetHeight,
|
||||
prop = resizer.key + resizer.mode;
|
||||
(value == Local.get(prop)) || Local.set(prop, value);
|
||||
(value == SettingsGet('Resizer' + prop)) || save({['Resizer' + prop]: value});
|
||||
|
|
@ -285,49 +258,38 @@ populateMessageBody = (oMessage, popup) => {
|
|||
}
|
||||
} else {
|
||||
let json = oData?.Result;
|
||||
|
||||
if (
|
||||
json &&
|
||||
MessageModel.validJson(json) &&
|
||||
oMessage.folder === json.folder
|
||||
if (json
|
||||
&& MessageModel.validJson(json)
|
||||
&& oMessage.hash === json.hash
|
||||
// && 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) {
|
||||
delete json.flags;
|
||||
}
|
||||
if (bCached) {
|
||||
delete json.flags;
|
||||
}
|
||||
*/
|
||||
isNew || oMessage.revivePropertiesFromJson(json);
|
||||
|
||||
if (popup) {
|
||||
oMessage.viewPopupMessage();
|
||||
} else {
|
||||
MessageUserStore.error('');
|
||||
const messagesDom = MessageUserStore.bodiesDom();
|
||||
if (messagesDom) {
|
||||
let id = 'rl-msg-' + oMessage.hash.replace(/[^a-zA-Z0-9]/g, ''),
|
||||
let id = 'rl-msg-' + oMessage.hash,
|
||||
body = elementById(id);
|
||||
if (body) {
|
||||
oMessage.body = body;
|
||||
oMessage.isHtml(body.classList.contains('html'));
|
||||
oMessage.hasImages(body.rlHasImages);
|
||||
} else {
|
||||
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
|
||||
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
|
||||
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||
+ '">'
|
||||
+ '</div>');
|
||||
body = createElement('div',{
|
||||
id:id,
|
||||
hidden:'',
|
||||
class:'b-text-part'
|
||||
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
|
||||
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||
});
|
||||
oMessage.body = body;
|
||||
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
|
||||
oMessage.viewPlain();
|
||||
|
|
@ -338,42 +300,16 @@ populateMessageBody = (oMessage, popup) => {
|
|||
|
||||
messagesDom.append(body);
|
||||
|
||||
popup || (oMessage.body.hidden = false);
|
||||
popup && oMessage.viewPopupMessage();
|
||||
oMessage.body.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
MessageFlagsCache.initMessage(oMessage);
|
||||
if (oMessage.isUnseen()) {
|
||||
MessageUserStore.MessageSeenTimer = setTimeout(
|
||||
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
MessageFlagsCache.initMessage(oMessage);
|
||||
if (oMessage.isUnseen()) {
|
||||
MessageUserStore.MessageSeenTimer = setTimeout(
|
||||
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
|
||||
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,29 @@
|
|||
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { EmailModel, addressparser } from 'Model/Email';
|
||||
|
||||
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;
|
||||
|
||||
|
|
@ -154,8 +175,8 @@ export class EmailAddressesComponent {
|
|||
if (val) {
|
||||
const self = this,
|
||||
v = val.trim(),
|
||||
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? EmailModel.splitEmailLine(val) : null,
|
||||
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
|
||||
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? splitEmailLine(val) : null,
|
||||
values = (hook || [val]).map(value => parseEmailLine(value))
|
||||
.flat(Infinity)
|
||||
.map(item => (item.toLine ? [item.toLine(), item] : [item, null]));
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export class AbstractViewPopup extends AbstractView
|
|||
super('Popups' + name, ViewTypePopup);
|
||||
this.keyScope.scope = name;
|
||||
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
|
||||
this.close = () => this.modalVisible(false);
|
||||
addShortcut('escape,close', '', name, () => {
|
||||
if (this.modalVisible() && false !== this.onClose()) {
|
||||
this.close();
|
||||
|
|
@ -78,8 +79,6 @@ export class AbstractViewPopup extends AbstractView
|
|||
afterShow() {} // Happens after showModal() animation transitionend
|
||||
onHide() {} // Happens before animation transitionend
|
||||
afterHide() {} // Happens after animation transitionend
|
||||
|
||||
close() {}
|
||||
*/
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import ko from '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 { i18nToNodes } from 'Common/Translator';
|
||||
|
||||
|
|
@ -40,22 +40,20 @@ const
|
|||
ViewModelClass.__vm = vm;
|
||||
|
||||
if (vmPlace) {
|
||||
vmDom = Element.fromHTML(dialog
|
||||
? '<dialog id="V-'+ id + '"></dialog>'
|
||||
: '<div id="V-'+ id + '" hidden=""></div>');
|
||||
vmDom = dialog
|
||||
? createElement('dialog',{id:'V-'+id})
|
||||
: createElement('div',{id:'V-'+id,hidden:''})
|
||||
vmPlace.append(vmDom);
|
||||
|
||||
vm.viewModelDom = ViewModelClass.__dom = vmDom;
|
||||
|
||||
if (dialog) {
|
||||
vm.close = () => hideScreenPopup(ViewModelClass);
|
||||
|
||||
// Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined
|
||||
if (!vmDom.showModal) {
|
||||
vmDom.className = 'polyfill';
|
||||
vmDom.showModal = () => {
|
||||
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.open = true;
|
||||
vmDom.returnValue = null;
|
||||
|
|
@ -155,81 +153,64 @@ const
|
|||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Function} ViewModelClassToHide
|
||||
* @returns {void}
|
||||
*/
|
||||
hideScreenPopup = ViewModelClassToHide =>
|
||||
ViewModelClassToHide?.__vm && ViewModelClassToHide.__dom
|
||||
&& ViewModelClassToHide.__vm.modalVisible(false),
|
||||
|
||||
/**
|
||||
* @param {string} screenName
|
||||
* @param {string} subPart
|
||||
* @returns {void}
|
||||
*/
|
||||
screenOnRoute = (screenName, subPart) => {
|
||||
let vmScreen = null,
|
||||
isSameScreen = false;
|
||||
|
||||
if (null == screenName || '' == screenName) {
|
||||
screenName = defaultScreenName;
|
||||
}
|
||||
|
||||
if (fireEvent('sm-show-screen', screenName, 1)) {
|
||||
|
||||
screenName = screenName || defaultScreenName;
|
||||
if (screenName && fireEvent('sm-show-screen', screenName, 1)) {
|
||||
// Close all popups
|
||||
for (let vm of visiblePopups) {
|
||||
(false === vm.onClose()) || vm.close();
|
||||
}
|
||||
|
||||
if (screenName) {
|
||||
vmScreen = screen(screenName);
|
||||
if (!vmScreen) {
|
||||
vmScreen = screen(defaultScreenName);
|
||||
if (vmScreen) {
|
||||
subPart = screenName + '/' + subPart;
|
||||
screenName = defaultScreenName;
|
||||
}
|
||||
let vmScreen = screen(screenName);
|
||||
if (!vmScreen) {
|
||||
vmScreen = screen(defaultScreenName);
|
||||
if (vmScreen) {
|
||||
subPart = screenName + '/' + subPart;
|
||||
screenName = defaultScreenName;
|
||||
}
|
||||
}
|
||||
|
||||
if (vmScreen?.__started) {
|
||||
let isSameScreen = currentScreen && vmScreen === currentScreen;
|
||||
|
||||
if (!vmScreen.__builded) {
|
||||
vmScreen.__builded = true;
|
||||
|
||||
vmScreen.viewModels.forEach(ViewModelClass =>
|
||||
buildViewModel(ViewModelClass, vmScreen)
|
||||
);
|
||||
|
||||
vmScreen.onBuild?.();
|
||||
}
|
||||
|
||||
if (vmScreen?.__started) {
|
||||
isSameScreen = currentScreen && vmScreen === currentScreen;
|
||||
setTimeout(() => {
|
||||
// hide screen
|
||||
currentScreen && !isSameScreen && hideScreen(currentScreen);
|
||||
// --
|
||||
|
||||
if (!vmScreen.__builded) {
|
||||
vmScreen.__builded = true;
|
||||
currentScreen = vmScreen;
|
||||
|
||||
vmScreen.viewModels.forEach(ViewModelClass =>
|
||||
buildViewModel(ViewModelClass, vmScreen)
|
||||
);
|
||||
// show screen
|
||||
if (!isSameScreen) {
|
||||
vmScreen.onShow?.();
|
||||
|
||||
vmScreen.onBuild?.();
|
||||
forEachViewModel(vmScreen, (vm, dom) => {
|
||||
vm.beforeShow?.();
|
||||
i18nToNodes(dom);
|
||||
dom.hidden = false;
|
||||
vm.onShow?.();
|
||||
autofocus(dom);
|
||||
});
|
||||
}
|
||||
// --
|
||||
|
||||
setTimeout(() => {
|
||||
// hide screen
|
||||
currentScreen && !isSameScreen && hideScreen(currentScreen);
|
||||
// --
|
||||
|
||||
currentScreen = vmScreen;
|
||||
|
||||
// show screen
|
||||
if (!isSameScreen) {
|
||||
vmScreen.onShow?.();
|
||||
|
||||
forEachViewModel(vmScreen, (vm, dom) => {
|
||||
vm.beforeShow?.();
|
||||
i18nToNodes(dom);
|
||||
dom.hidden = false;
|
||||
vm.onShow?.();
|
||||
autofocus(dom);
|
||||
});
|
||||
}
|
||||
// --
|
||||
|
||||
vmScreen.__cross?.parse(subPart);
|
||||
}, 1);
|
||||
}
|
||||
vmScreen.__cross?.parse(subPart);
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -285,7 +266,7 @@ export const
|
|||
});
|
||||
|
||||
const cross = new Crossroads();
|
||||
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
|
||||
cross.addRoute(/^([^/]*)\/?(.*)$/, screenOnRoute);
|
||||
|
||||
hasher.add(cross.parse.bind(cross));
|
||||
hasher.init();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
import { ParseMime } from 'Mime/Parser';
|
||||
import { AttachmentModel } from 'Model/Attachment';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { FileInfo } from 'Common/File';
|
||||
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
|
||||
|
||||
|
|
@ -17,16 +16,10 @@ export function MimeToMessage(data, message)
|
|||
let html = struct.getByContentType('text/html');
|
||||
html = html ? html.body : '';
|
||||
|
||||
if (struct.headers.subject) {
|
||||
message.subject(struct.headers.subject.value);
|
||||
}
|
||||
['from','to'].forEach(name => {
|
||||
if (struct.headers[name] && !message[name].length) {
|
||||
let mail = new EmailModel;
|
||||
mail.parse(struct.headers[name].value);
|
||||
message[name].push(mail);
|
||||
}
|
||||
});
|
||||
message.subject(struct.headerValue('subject') || '');
|
||||
|
||||
// EmailCollectionModel
|
||||
['from','to'].forEach(name => message[name].fromString(struct.headerValue(name)));
|
||||
|
||||
struct.forEach(part => {
|
||||
let cd = part.header('content-disposition'),
|
||||
|
|
@ -40,7 +33,7 @@ export function MimeToMessage(data, message)
|
|||
attachment.fileNameExt = attachment.fileName.replace(/^.+(\.[a-z]+)$/, '$1');
|
||||
attachment.fileType = FileInfo.getType('', type.value);
|
||||
attachment.url = part.dataUrl;
|
||||
attachment.friendlySize = FileInfo.friendlySize(part.body.length);
|
||||
attachment.estimatedSize = part.body.length;
|
||||
/*
|
||||
attachment.isThumbnail = false;
|
||||
attachment.contentLocation = '';
|
||||
|
|
|
|||
|
|
@ -18,32 +18,83 @@ import { AbstractModel } from 'Knoin/AbstractModel';
|
|||
* @param {String} str Address field
|
||||
* @return {Array} An array of address objects
|
||||
*/
|
||||
function addressparser(str) {
|
||||
var tokenizer = new Tokenizer(str);
|
||||
var tokens = tokenizer.tokenize();
|
||||
var addresses = [];
|
||||
var address = [];
|
||||
var parsedAddresses = [];
|
||||
export function addressparser(str) {
|
||||
str = (str || '').toString();
|
||||
|
||||
tokens.forEach(token => {
|
||||
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
|
||||
address.length && addresses.push(address);
|
||||
let
|
||||
endOperator = '',
|
||||
node = {
|
||||
type: 'text',
|
||||
value: ''
|
||||
},
|
||||
escaped = false,
|
||||
address = [],
|
||||
addresses = [];
|
||||
|
||||
const
|
||||
/*
|
||||
* Operator tokens and which tokens are expected to end the sequence
|
||||
*/
|
||||
OPERATORS = {
|
||||
'"': '"',
|
||||
'(': ')',
|
||||
'<': '>',
|
||||
',': '',
|
||||
// Groups are ended by semicolons
|
||||
':': ';',
|
||||
// Semicolons are not a legal delimiter per the RFC2822 grammar other
|
||||
// than for terminating a group, but they are also not valid for any
|
||||
// other use in this context. Given that some mail clients have
|
||||
// historically allowed the semicolon as a delimiter equivalent to the
|
||||
// comma in their UI, it makes sense to treat them the same as a comma
|
||||
// when used outside of a group.
|
||||
';': ''
|
||||
},
|
||||
pushToken = token => {
|
||||
token.value = (token.value || '').toString().trim();
|
||||
token.value.length && address.push(token);
|
||||
node = {
|
||||
type: 'text',
|
||||
value: ''
|
||||
},
|
||||
escaped = false;
|
||||
},
|
||||
pushAddress = () => {
|
||||
if (address.length) {
|
||||
address = _handleAddress(address);
|
||||
if (address.length) {
|
||||
addresses = addresses.concat(address);
|
||||
}
|
||||
}
|
||||
address = [];
|
||||
};
|
||||
|
||||
[...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 {
|
||||
address.push(token);
|
||||
node.value += chr;
|
||||
escaped = !escaped && '\\' === chr;
|
||||
}
|
||||
});
|
||||
pushToken(node);
|
||||
|
||||
address.length && addresses.push(address);
|
||||
pushAddress();
|
||||
|
||||
addresses.forEach(address => {
|
||||
address = _handleAddress(address);
|
||||
if (address.length) {
|
||||
parsedAddresses = parsedAddresses.concat(address);
|
||||
}
|
||||
});
|
||||
|
||||
return parsedAddresses;
|
||||
return addresses;
|
||||
// return addresses.map(item => (item.name || item.email) ? new EmailModel(item.email, item.name) : null).filter(v => v);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -53,37 +104,20 @@ function addressparser(str) {
|
|||
* @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: []
|
||||
};
|
||||
let
|
||||
isGroup = false,
|
||||
address = {},
|
||||
addresses = [],
|
||||
data = {
|
||||
email: [],
|
||||
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);
|
||||
}
|
||||
isGroup = isGroup || 'group' === token.type;
|
||||
data[token.type].push(token.value);
|
||||
});
|
||||
|
||||
// If there is no text but a comment, replace the two
|
||||
|
|
@ -94,179 +128,90 @@ function _handleAddress(tokens) {
|
|||
|
||||
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(',')) : []
|
||||
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.address.length && data.text.length) {
|
||||
if (!data.email.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);
|
||||
data.email = data.text.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// still no address
|
||||
if (!data.address.length) {
|
||||
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.address.length) {
|
||||
data.address = [address.trim()];
|
||||
if (!data.email.length) {
|
||||
data.email = [address.trim()];
|
||||
return '';
|
||||
}
|
||||
return address.trim();
|
||||
});
|
||||
if (data.address.length) {
|
||||
if (data.email.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's still is no text but a comment exixts, replace the two
|
||||
// If there's still no text but a comment exists, replace the two
|
||||
if (!data.text.length && data.comment.length) {
|
||||
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));
|
||||
if (data.email.length > 1) {
|
||||
data.text = data.text.concat(data.email.splice(1));
|
||||
}
|
||||
|
||||
// Join values with spaces
|
||||
data.text = data.text.join(' ');
|
||||
data.address = data.address.join(' ');
|
||||
|
||||
if (!data.address && isGroup) {
|
||||
return [];
|
||||
}
|
||||
address = {
|
||||
address: data.address || data.text || '',
|
||||
name: data.text || data.address || ''
|
||||
// Join values with spaces
|
||||
email: data.email.join(' ').trim(),
|
||||
name: data.text.join(' ').trim()
|
||||
// ,comment: data.comment.join(' ').trim()
|
||||
};
|
||||
|
||||
if (address.address === address.name) {
|
||||
if ((address.address || '').match(/@/)) {
|
||||
if (address.email === address.name) {
|
||||
if (address.email.includes('@')) {
|
||||
address.name = '';
|
||||
} else {
|
||||
address.address = '';
|
||||
address.email = '';
|
||||
}
|
||||
}
|
||||
|
||||
// address.email = address.email.replace(/^[<]+(.*)[>]+$/g, '$1');
|
||||
|
||||
addresses.push(address);
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
/*
|
||||
* Operator tokens and which tokens are expected to end the sequence
|
||||
*/
|
||||
var OPERATORS = {
|
||||
'"': '"',
|
||||
'(': ')',
|
||||
'<': '>',
|
||||
',': '',
|
||||
// Groups are ended by semicolons
|
||||
':': ';',
|
||||
// Semicolons are not a legal delimiter per the RFC2822 grammar other
|
||||
// than for terminating a group, but they are also not valid for any
|
||||
// other use in this context. Given that some mail clients have
|
||||
// historically allowed the semicolon as a delimiter equivalent to the
|
||||
// comma in their UI, it makes sense to treat them the same as a comma
|
||||
// when used outside of a group.
|
||||
';': ''
|
||||
};
|
||||
|
||||
class Tokenizer
|
||||
{
|
||||
constructor(str) {
|
||||
this.str = (str || '').toString();
|
||||
this.operatorCurrent = '';
|
||||
this.operatorExpecting = '';
|
||||
this.node = null;
|
||||
this.escaped = false;
|
||||
this.list = [];
|
||||
}
|
||||
|
||||
tokenize() {
|
||||
var list = [];
|
||||
[...this.str].forEach(c => this.checkChar(c));
|
||||
|
||||
this.list.forEach(node => {
|
||||
node.value = (node.value || '').toString().trim();
|
||||
node.value && list.push(node);
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
checkChar(chr) {
|
||||
if ((chr in OPERATORS || chr === '\\') && this.escaped) {
|
||||
this.escaped = false;
|
||||
} else if (this.operatorExpecting && chr === this.operatorExpecting) {
|
||||
this.node = {
|
||||
type: 'operator',
|
||||
value: chr
|
||||
};
|
||||
this.list.push(this.node);
|
||||
this.node = null;
|
||||
this.operatorExpecting = '';
|
||||
this.escaped = false;
|
||||
return;
|
||||
} else if (!this.operatorExpecting && chr in OPERATORS) {
|
||||
this.node = {
|
||||
type: 'operator',
|
||||
value: chr
|
||||
};
|
||||
this.list.push(this.node);
|
||||
this.node = null;
|
||||
this.operatorExpecting = OPERATORS[chr];
|
||||
this.escaped = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.escaped && chr === '\\') {
|
||||
this.escaped = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.node) {
|
||||
this.node = {
|
||||
type: 'text',
|
||||
value: ''
|
||||
};
|
||||
this.list.push(this.node);
|
||||
}
|
||||
|
||||
if (this.escaped && chr !== '\\') {
|
||||
this.node.value += '\\';
|
||||
}
|
||||
|
||||
this.node.value += chr;
|
||||
this.escaped = false;
|
||||
}
|
||||
}
|
||||
|
||||
export class EmailModel extends AbstractModel {
|
||||
/**
|
||||
* @param {string=} email = ''
|
||||
* @param {string=} name = ''
|
||||
* @param {string=} dkimStatus = 'none'
|
||||
*/
|
||||
constructor(email = '', name = '', dkimStatus = 'none') {
|
||||
constructor(email, name, dkimStatus = 'none') {
|
||||
super();
|
||||
this.email = email;
|
||||
this.name = name;
|
||||
this.email = email || '';
|
||||
this.name = name || '';
|
||||
this.dkimStatus = dkimStatus;
|
||||
|
||||
this.clearDuplicateName();
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -276,60 +221,34 @@ export class EmailModel extends AbstractModel {
|
|||
*/
|
||||
static reviveFromJson(json) {
|
||||
const email = super.reviveFromJson(json);
|
||||
email?.clearDuplicateName();
|
||||
return email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
clear() {
|
||||
this.email = '';
|
||||
this.name = '';
|
||||
|
||||
this.dkimStatus = 'none';
|
||||
email?.cleanup();
|
||||
return email?.valid() ? email : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validate() {
|
||||
valid() {
|
||||
return this.name || this.email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} withoutName = false
|
||||
* @returns {string}
|
||||
*/
|
||||
hash(withoutName = false) {
|
||||
return '#' + (withoutName ? '' : this.name) + '#' + this.email + '#';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
clearDuplicateName() {
|
||||
cleanup() {
|
||||
if (this.name === this.email) {
|
||||
this.name = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} query
|
||||
* @returns {boolean}
|
||||
*/
|
||||
search(query) {
|
||||
return (this.name + ' ' + this.email).toLowerCase().includes(query.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} friendlyView = false
|
||||
* @param {boolean} wrapWithLink = false
|
||||
* @returns {string}
|
||||
*/
|
||||
toLine(friendlyView, wrapWithLink) {
|
||||
let result = this.email,
|
||||
name = this.name,
|
||||
let name = this.name,
|
||||
result = this.email,
|
||||
toLink = text =>
|
||||
'<a href="mailto:'
|
||||
+ encodeHtml(result) + (name ? '?to=' + encodeURIComponent('"' + name + '" <' + result + '>') : '')
|
||||
|
|
@ -348,47 +267,6 @@ export class EmailModel extends AbstractModel {
|
|||
result = toLink();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
return result || name;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { EmailModel, addressparser } from 'Model/Email';
|
||||
import { forEachObjectValue } from 'Common/Utils';
|
||||
|
||||
'use strict';
|
||||
|
||||
|
|
@ -13,6 +14,16 @@ export class EmailCollectionModel extends AbstractCollectionModel
|
|||
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=} wrapWithLink = false
|
||||
|
|
@ -21,4 +32,23 @@ export class EmailCollectionModel extends AbstractCollectionModel
|
|||
toString(friendlyView, wrapWithLink) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { i18n } from 'Common/Translator';
|
|||
|
||||
import { doc, SettingsGet } from 'Common/Globals';
|
||||
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 { addObservablesTo, addComputablesTo } from 'External/ko';
|
||||
|
||||
|
|
@ -25,6 +25,8 @@ import { LanguageStore } from 'Stores/Language';
|
|||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
const
|
||||
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
|
||||
|
||||
toggleTag = (message, keyword) => {
|
||||
const lower = keyword.toLowerCase(),
|
||||
flags = message.flags,
|
||||
|
|
@ -212,6 +214,7 @@ export class MessageModel extends AbstractModel {
|
|||
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.attachments, this.foundCIDs));
|
||||
|
||||
this.computeSenderEmail();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,7 +293,7 @@ export class MessageModel extends AbstractModel {
|
|||
viewHtml() {
|
||||
const body = this.body;
|
||||
if (body && this.html()) {
|
||||
let result = cleanHtml(this.html(), this.attachments());
|
||||
let result = msgHtml(this);
|
||||
this.hasExternals(result.hasExternals);
|
||||
this.hasImages(body.rlHasImages = !!result.hasExternals);
|
||||
|
||||
|
|
@ -300,9 +303,7 @@ export class MessageModel extends AbstractModel {
|
|||
body.classList.toggle('plain', 0);
|
||||
|
||||
if (!this.isSpam() && FolderUserStore.spamFolder() != this.folder) {
|
||||
if (('dkim' === SettingsUserStore.viewImages() && 'pass' === this.dkim[0]?.[0])
|
||||
|| 'always' === SettingsUserStore.viewImages()
|
||||
) {
|
||||
if ('always' === SettingsUserStore.viewImages()) {
|
||||
this.showExternalImages();
|
||||
}
|
||||
if ('match' === SettingsUserStore.viewImages()) {
|
||||
|
|
@ -335,26 +336,30 @@ export class MessageModel extends AbstractModel {
|
|||
}
|
||||
|
||||
viewPopupMessage(print) {
|
||||
const timeStampInUTC = this.dateTimeStampInUTC() || 0,
|
||||
const
|
||||
timeStampInUTC = this.dateTimeStampInUTC() || 0,
|
||||
ccLine = this.cc.toString(),
|
||||
bccLine = this.bcc.toString(),
|
||||
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
|
||||
win = open(''),
|
||||
sdoc = win.document;
|
||||
let subject = encodeHtml(this.subject()),
|
||||
win = open('', 'sm-msg-'+this.requestHash
|
||||
/*,newWindow ? 'innerWidth=' + elementById('V-MailMessageView').clientWidth : ''*/
|
||||
),
|
||||
sdoc = win.document,
|
||||
subject = encodeHtml(this.subject()),
|
||||
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')),
|
||||
prop = property => style.getPropertyValue(property);
|
||||
sdoc.write(PreviewHTML
|
||||
.replace('<title>', '<title>'+subject)
|
||||
// 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();
|
||||
|
||||
if (print) {
|
||||
setTimeout(() => win.print(), 100);
|
||||
}
|
||||
print && setTimeout(() => win.print(), 100);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -376,45 +381,50 @@ export class MessageModel extends AbstractModel {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {MessageModel} message
|
||||
* @returns {MessageModel}
|
||||
*/
|
||||
static fromMessageListItem(message) {
|
||||
*//*
|
||||
clone() {
|
||||
let self = new MessageModel();
|
||||
if (message) {
|
||||
// Clone message values
|
||||
forEachObjectEntry(message, (key, value) => {
|
||||
if (ko.isObservable(value)) {
|
||||
ko.isComputed(value) || self[key](value());
|
||||
} else if (!isFunction(value)) {
|
||||
self[key] = value;
|
||||
}
|
||||
});
|
||||
self.computeSenderEmail();
|
||||
}
|
||||
// Clone message values
|
||||
forEachObjectEntry(this, (key, value) => {
|
||||
if (ko.isObservable(value)) {
|
||||
ko.isComputed(value) || self[key](value());
|
||||
} else if (!isFunction(value)) {
|
||||
self[key] = value;
|
||||
}
|
||||
});
|
||||
self.computeSenderEmail();
|
||||
return self;
|
||||
}
|
||||
}*/
|
||||
|
||||
showExternalImages(regex) {
|
||||
const body = this.body;
|
||||
if (body && this.hasImages()) {
|
||||
if (regex) {
|
||||
regex = SettingsUserStore.viewImagesWhitelist()
|
||||
.trim()
|
||||
.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
|
||||
.replace(/[\s\r\n,;:]+/g, '|')
|
||||
.replace(/\|+/g, '|');
|
||||
regex = [];
|
||||
SettingsUserStore.viewImagesWhitelist().trim().split(/[\s\r\n,;]+/g).forEach(rule => {
|
||||
rule = rule.split('+');
|
||||
rule[0] = rule[0].trim();
|
||||
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) {
|
||||
console.log('whitelist images = '+regex);
|
||||
regex = new RegExp(regex);
|
||||
if (this.from[0]?.email.match(regex)) {
|
||||
regex = 0;
|
||||
regex = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
let hasImages = false,
|
||||
isValid = src => {
|
||||
if (!regex || src.match(regex)) {
|
||||
if (null == regex || (regex && src.match(regex))) {
|
||||
return true;
|
||||
}
|
||||
hasImages = true;
|
||||
|
|
@ -452,7 +462,7 @@ export class MessageModel extends AbstractModel {
|
|||
);
|
||||
return clone.innerHTML;
|
||||
}
|
||||
let result = cleanHtml(this.html(), this.attachments())
|
||||
let result = msgHtml(this);
|
||||
return result.html || plainToHtml(this.plain());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||
import { MessageModel } from 'Model/Message';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
|
||||
import {
|
||||
MessageFlagsCache
|
||||
|
|
@ -31,8 +32,11 @@ export class MessageCollectionModel extends AbstractCollectionModel
|
|||
* @returns {MessageCollectionModel}
|
||||
*/
|
||||
static reviveFromJson(object, cached) {
|
||||
let msg = MessageUserStore.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) {
|
||||
message.deleted(false);
|
||||
cached ? MessageFlagsCache.initMessage(message) : MessageFlagsCache.store(message);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { Scope } from 'Common/Enums';
|
||||
import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser';
|
||||
import { doc, createElement, leftPanelDisabled, Settings, elementById } from 'Common/Globals';
|
||||
import { doc, createElement, leftPanelDisabled, Settings } from 'Common/Globals';
|
||||
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 { i18n, initOnStartOrLangChange } from 'Common/Translator';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { AccountUserStore } from 'Stores/User/Account';
|
||||
|
|
@ -113,24 +111,6 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
* @returns {void}
|
||||
*/
|
||||
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 =>
|
||||
event.target.closest('#rl-right') && moveAction(false)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
|
||||
['layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
|
||||
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
|
||||
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors',
|
||||
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles',
|
||||
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes', 'maxBlockquotesLevel',
|
||||
'useCheckboxesInList', 'listGrouped', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck'
|
||||
].forEach(name => this[name] = SettingsUserStore[name]);
|
||||
|
|
@ -102,7 +102,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
this.addSetting('Layout');
|
||||
this.addSetting('MaxBlockquotesLevel');
|
||||
|
||||
this.addSettings(['ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'HideDeleted',
|
||||
this.addSettings(['ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'HideDeleted', 'AllowStyles',
|
||||
'ListInlineAttachments', 'simpleAttachmentsList', 'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder',
|
||||
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'allowSpellcheck',
|
||||
'DesktopNotifications', 'SoundNotification', 'CollapseBlockquotes']);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const SettingsUserStore = new class {
|
|||
viewImages: 0,
|
||||
viewImagesWhitelist: '',
|
||||
removeColors: 0,
|
||||
allowStyles: 0,
|
||||
collapseBlockquotes: 1,
|
||||
maxBlockquotesLevel: 0,
|
||||
listInlineAttachments: 0,
|
||||
|
|
@ -88,6 +89,7 @@ export const SettingsUserStore = new class {
|
|||
self.viewImages(SettingsGet('ViewImages'));
|
||||
self.viewImagesWhitelist(SettingsGet('ViewImagesWhitelist'));
|
||||
self.removeColors(SettingsGet('RemoveColors'));
|
||||
self.allowStyles(SettingsGet('AllowStyles'));
|
||||
self.collapseBlockquotes(SettingsGet('CollapseBlockquotes'));
|
||||
self.maxBlockquotesLevel(SettingsGet('MaxBlockquotesLevel'));
|
||||
self.listInlineAttachments(SettingsGet('ListInlineAttachments'));
|
||||
|
|
|
|||
|
|
@ -62,9 +62,14 @@
|
|||
#rl-left {
|
||||
display: none;
|
||||
}
|
||||
#rl-content, #rl-right {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.message-selected {
|
||||
.close,
|
||||
.infoParent,
|
||||
.top-toolbar,
|
||||
.messageView .controls-handle,
|
||||
#tags-dropdown-id,
|
||||
.message-fixed-button-toolbar,
|
||||
|
|
@ -75,6 +80,7 @@
|
|||
}
|
||||
#V-MailMessageView {
|
||||
left: 0;
|
||||
overflow: initial;
|
||||
position: initial;
|
||||
}
|
||||
.messageView,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ option:disabled {
|
|||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
#rl-content {
|
||||
#rl-content:not([hidden]) {
|
||||
display:flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@
|
|||
z-index: 0;
|
||||
}
|
||||
|
||||
#rl-right {
|
||||
#rl-right:not([hidden]) {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
max-width: 100vw;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 20%;
|
||||
}
|
||||
.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-side-preview-pane #V-MailMessageList > .resizer {
|
||||
.rl-side-preview-pane #V-MailMessageList .resizer {
|
||||
cursor: ew-resize;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
.rl-bottom-preview-pane #V-MailMessageList .resizer {
|
||||
cursor: ns-resize;
|
||||
height: 5px;
|
||||
left: 0;
|
||||
cursor: row-resize;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
html:not(.rl-left-panel-disabled) #rl-left > .resizer,
|
||||
#V-MailMessageList > .resizer {
|
||||
#V-MailMessageList .resizer {
|
||||
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,
|
||||
#button-add-prop-dropdown-id::after {
|
||||
content: '▼';
|
||||
|
|
@ -201,16 +149,6 @@ html.rl-side-preview-pane {
|
|||
#rl-left {
|
||||
width: @rlLeftWidth + 20;
|
||||
}
|
||||
|
||||
#V-MailMessageList {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
html.rl-side-preview-pane {
|
||||
#V-MailMessageView {
|
||||
left: 40%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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-no-preview-pane #rl-right .resizer {
|
||||
display: none !important;
|
||||
|
|
@ -278,28 +205,13 @@ html.rl-mobile {
|
|||
top: 0;
|
||||
bottom: 0;
|
||||
width: 50vw;
|
||||
z-index: 2;
|
||||
z-index: 3;
|
||||
}
|
||||
&.rl-left-panel-disabled #rl-left {
|
||||
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:not(.rl-mobile) .show-mobile {
|
||||
display: none !important;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
border-color: #9d9d9d;
|
||||
box-shadow: var(--smMainShadow);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
#rl-right:not(.message-selected) #V-MailMessageView {
|
||||
display: none;
|
||||
|
|
@ -317,13 +348,13 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
|
|||
|
||||
.sm-bq-switcher > summary {
|
||||
border: 1px solid #999;
|
||||
display: block;
|
||||
width: 3em;
|
||||
line-height: 1em;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
line-height: 1em;
|
||||
margin: 2em 0 10px;
|
||||
opacity: 0.5;
|
||||
padding: 0 0.5em;
|
||||
text-align: center;
|
||||
width: fit-content;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
#V-SettingsPane {
|
||||
height: calc(100% - 50px - @rlLowMargin);
|
||||
margin: 50px @rlLowMargin 0 0;
|
||||
width: 100%;
|
||||
|
||||
.btn-toolbar {
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
|||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { EmailModel, addressparser } from 'Model/Email';
|
||||
|
||||
import { decorateKoCommands, showScreenPopup } from 'Knoin/Knoin';
|
||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||
|
|
@ -59,12 +59,7 @@ const
|
|||
|
||||
base64_encode = text => btoa(unescape(encodeURIComponent(text))).match(/.{1,76}/g).join('\r\n'),
|
||||
|
||||
email = new EmailModel(),
|
||||
getEmail = value => {
|
||||
email.clear();
|
||||
email.parse(value.trim());
|
||||
return email.email || false;
|
||||
},
|
||||
getEmail = value => addressparser(value)[0]?.email || false,
|
||||
|
||||
/**
|
||||
* @param {Array} aList
|
||||
|
|
@ -72,7 +67,7 @@ const
|
|||
* @returns {string}
|
||||
*/
|
||||
emailArrayToStringLineHelper = (aList, bFriendly) =>
|
||||
aList.map(item => item.toLine(bFriendly)).join(', '),
|
||||
aList.filter(item => item.email).map(item => item.toLine(bFriendly)).join(', '),
|
||||
|
||||
reloadDraftFolder = () => {
|
||||
const draftsFolder = FolderUserStore.draftsFolder();
|
||||
|
|
@ -205,10 +200,6 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
};
|
||||
|
||||
this.oEditor = null;
|
||||
this.aDraftInfo = null;
|
||||
this.sInReplyTo = '';
|
||||
this.bFromDraft = false;
|
||||
this.sReferences = '';
|
||||
|
||||
this.sLastFocusedField = 'to';
|
||||
|
||||
|
|
@ -854,33 +845,17 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
break;
|
||||
|
||||
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.draftsFolder(message.folder);
|
||||
this.draftUid(message.uid);
|
||||
|
||||
this.subject(sSubject);
|
||||
this.prepareMessageAttachments(message, msgComposeType);
|
||||
|
||||
this.aDraftInfo = 3 === arrayLength(aDraftInfo) ? aDraftInfo : null;
|
||||
this.sInReplyTo = message.inReplyTo;
|
||||
this.sReferences = message.references;
|
||||
break;
|
||||
|
||||
// fallthrough
|
||||
case ComposeType.EditAsNew:
|
||||
this.to(emailArrayToStringLineHelper(message.to));
|
||||
this.cc(emailArrayToStringLineHelper(message.cc));
|
||||
this.bcc(emailArrayToStringLineHelper(message.bcc));
|
||||
this.replyTo(emailArrayToStringLineHelper(message.replyTo));
|
||||
|
||||
this.subject(sSubject);
|
||||
this.prepareMessageAttachments(message, msgComposeType);
|
||||
|
||||
this.aDraftInfo = 3 === arrayLength(aDraftInfo) ? aDraftInfo : null;
|
||||
this.sInReplyTo = message.inReplyTo;
|
||||
this.sReferences = message.references;
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
this.contactsCheckedOrSelected().forEach(oContact => {
|
||||
const data = oContact?.getNameAndEmailHelper(),
|
||||
email = data ? new EmailModel(data[0], data[1]) : null;
|
||||
email?.validate() && aE.push(email);
|
||||
email?.valid() && aE.push(email);
|
||||
});
|
||||
|
||||
if (arrayLength(aE)) {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ import { ComposePopupView } from 'View/Popup/Compose';
|
|||
|
||||
import { MessageModel } from 'Model/Message';
|
||||
|
||||
import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser';
|
||||
import { setLayoutResizer } from 'Common/UtilsUser';
|
||||
|
||||
const
|
||||
canBeMovedHelper = () => MessagelistUserStore.hasCheckedOrSelected(),
|
||||
|
||||
|
|
@ -159,12 +162,12 @@ export class MailMessageList extends AbstractViewRight {
|
|||
let list = [], current, sort = FolderUserStore.sortMode() || 'DATE';
|
||||
if (sort.includes('FROM')) {
|
||||
MessagelistUserStore.forEach(msg => {
|
||||
let email = msg.from?.[0].email;
|
||||
let email = msg.from[0].email;
|
||||
if (!current || email != current.id) {
|
||||
current = {
|
||||
id: email,
|
||||
label: msg.from[0].toLine(),
|
||||
search: 'from=' + msg.from[0].email,
|
||||
search: 'from=' + email,
|
||||
messages: []
|
||||
};
|
||||
list.push(current);
|
||||
|
|
@ -232,7 +235,8 @@ export class MailMessageList extends AbstractViewRight {
|
|||
|
||||
this.selector.on('ItemSelect', message => {
|
||||
if (message) {
|
||||
populateMessageBody(MessageModel.fromMessageListItem(message));
|
||||
// populateMessageBody(message.clone());
|
||||
populateMessageBody(message);
|
||||
} else {
|
||||
MessageUserStore.message(null);
|
||||
}
|
||||
|
|
@ -564,6 +568,23 @@ export class MailMessageList extends AbstractViewRight {
|
|||
const b_content = dom.querySelector('.b-content'),
|
||||
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);
|
||||
|
||||
addEventsListeners(dom, {
|
||||
|
|
|
|||
|
|
@ -112,8 +112,7 @@ export class MailMessageView extends AbstractViewRight {
|
|||
|
||||
// viewer
|
||||
viewFromShort: '',
|
||||
viewFromDkimData: ['none', ''],
|
||||
viewToShort: ''
|
||||
dkimData: ['none', '', '']
|
||||
});
|
||||
|
||||
this.moveAction = moveAction;
|
||||
|
|
@ -171,10 +170,10 @@ export class MailMessageView extends AbstractViewRight {
|
|||
|
||||
canBeRepliedOrForwarded: () => !MessagelistUserStore.isDraftFolder() && this.messageVisibility(),
|
||||
|
||||
viewFromDkimVisibility: () => 'none' !== this.viewFromDkimData()[0],
|
||||
viewDkimIcon: () => 'none' !== this.dkimData()[0],
|
||||
|
||||
viewFromDkimStatusIconClass:() => {
|
||||
switch (this.viewFromDkimData()[0]) {
|
||||
dkimIconClass:() => {
|
||||
switch (this.dkimData()[0]) {
|
||||
case 'none':
|
||||
return '';
|
||||
case 'pass':
|
||||
|
|
@ -184,13 +183,9 @@ export class MailMessageView extends AbstractViewRight {
|
|||
}
|
||||
},
|
||||
|
||||
viewFromDkimStatusTitle:() => {
|
||||
const status = this.viewFromDkimData();
|
||||
if (arrayLength(status) && status[0]) {
|
||||
return status[1] || 'DKIM: ' + status[0];
|
||||
}
|
||||
|
||||
return '';
|
||||
dkimTitle:() => {
|
||||
const dkim = this.dkimData();
|
||||
return dkim[0] ? dkim[2] || 'DKIM: ' + dkim[0] : '';
|
||||
},
|
||||
|
||||
showWhitelistOptions: () => 'match' === SettingsUserStore.viewImages(),
|
||||
|
|
@ -212,10 +207,7 @@ export class MailMessageView extends AbstractViewRight {
|
|||
this.viewHash = message.hash;
|
||||
// TODO: make first param a user setting #683
|
||||
this.viewFromShort(message.from.toString(false, true));
|
||||
let dkim = 1 === arrayLength(message.from) && message.dkim
|
||||
&& 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));
|
||||
this.dkimData(message.dkim[0] || ['none', '', '']);
|
||||
} else {
|
||||
MessagelistUserStore.selectedMessage(null);
|
||||
|
||||
|
|
@ -503,6 +495,11 @@ export class MailMessageView extends AbstractViewRight {
|
|||
|
||||
whitelistText(txt) {
|
||||
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);
|
||||
Remote.saveSetting('ViewImagesWhitelist', value);
|
||||
currentMessage().showExternalImages(1);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ RUN mkdir -p /app/code
|
|||
WORKDIR /app/code
|
||||
|
||||
# If you change the extraction below, be sure to test on scaleway
|
||||
VERSION=2.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 && \
|
||||
unzip /tmp/snappymail.zip -d /app/code && \
|
||||
rm /tmp/snappymail.zip && \
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ From that point, all instance-wide SnappyMail settings can be tweaked as you wis
|
|||
|
||||

|
||||
|
||||
## 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?
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<id>snappymail</id>
|
||||
<name>SnappyMail</name>
|
||||
<summary>SnappyMail Webmail</summary>
|
||||
<version>2.25.4</version>
|
||||
<version>2.26.1</version>
|
||||
<licence>agpl</licence>
|
||||
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
|
||||
<description><![CDATA[**Simple, modern, lightweight & fast web-based email client.**
|
||||
|
|
|
|||
0
integrations/nextcloud/snappymail/appinfo/routes.php
Executable file → Normal file
0
integrations/nextcloud/snappymail/appinfo/routes.php
Executable file → Normal file
0
integrations/nextcloud/snappymail/templates/admin-local.php
Executable file → Normal file
0
integrations/nextcloud/snappymail/templates/admin-local.php
Executable file → Normal file
0
integrations/nextcloud/snappymail/templates/index.php
Executable file → Normal file
0
integrations/nextcloud/snappymail/templates/index.php
Executable file → Normal file
0
integrations/nextcloud/snappymail/templates/index_embed.php
Executable file → Normal file
0
integrations/nextcloud/snappymail/templates/index_embed.php
Executable file → Normal file
0
integrations/nextcloud/snappymail/templates/personal_settings.php
Executable file → Normal file
0
integrations/nextcloud/snappymail/templates/personal_settings.php
Executable file → Normal file
|
|
@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
|
|||
# script_snappymail_versions()
|
||||
sub script_snappymail_versions
|
||||
{
|
||||
return ( "2.25.4" );
|
||||
return ( "2.26.1" );
|
||||
}
|
||||
|
||||
sub script_snappymail_version_desc
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"title": "SnappyMail",
|
||||
"description": "Simple, modern & fast web-based email client",
|
||||
"private": true,
|
||||
"version": "2.25.4",
|
||||
"version": "2.26.1",
|
||||
"homepage": "https://snappymail.eu",
|
||||
"author": {
|
||||
"name": "DJ Maze",
|
||||
|
|
|
|||
|
|
@ -133,12 +133,16 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt
|
|||
$oParams->iLimit = 999; // Is the max
|
||||
$oMessageList = $this->MailClient()->MessageList($oParams);
|
||||
foreach ($oMessageList as $oMessage) {
|
||||
if ($rCsv) {
|
||||
$oContact = $this->MessageAsContact($oMessage);
|
||||
\RainLoop\Providers\AddressBook\Utils::VCardToCsv($rCsv, $oContact, $bCsvHeader);
|
||||
$bCsvHeader = false;
|
||||
} else if ($xCard = $this->fetchXCardFromMessage($oMessage)) {
|
||||
echo $xCard->serialize();
|
||||
try {
|
||||
if ($rCsv) {
|
||||
$oContact = $this->MessageAsContact($oMessage);
|
||||
\RainLoop\Providers\AddressBook\Utils::VCardToCsv($rCsv, $oContact->vCard, $bCsvHeader);
|
||||
$bCsvHeader = false;
|
||||
} else if ($xCard = $this->fetchXCardFromMessage($oMessage)) {
|
||||
echo $xCard->serialize();
|
||||
}
|
||||
} catch (\Throwable $oExc) {
|
||||
$this->oLogger && $this->oLogger->WriteException($oExc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +160,7 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt
|
|||
return false;
|
||||
}
|
||||
|
||||
$id = $oContact->id;
|
||||
$id = \intval($oContact->id);
|
||||
$sUID = '';
|
||||
|
||||
$oVCard = $oContact->vCard;
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ class KolabPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
{
|
||||
const
|
||||
NAME = 'Kolab',
|
||||
VERSION = '2.5',
|
||||
RELEASE = '2023-01-17',
|
||||
VERSION = '2.6',
|
||||
RELEASE = '2023-02-10',
|
||||
CATEGORY = 'Contacts',
|
||||
DESCRIPTION = 'Use an Address Book of Kolab.',
|
||||
REQUIRED = '2.24.6';
|
||||
REQUIRED = '2.26.0';
|
||||
|
||||
public function Init() : void
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ class LoginOverridePlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
{
|
||||
const
|
||||
NAME = 'Login Override',
|
||||
VERSION = '2.1',
|
||||
RELEASE = '2023-02-01',
|
||||
VERSION = '2.2',
|
||||
RELEASE = '2023-02-08',
|
||||
REQUIRED = '2.25.3',
|
||||
CATEGORY = 'Filters',
|
||||
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', ''));
|
||||
if (!empty($sMapping)) {
|
||||
$aList = \preg_split('/\\R/', $sMapping);
|
||||
foreach ($aList as $line) {
|
||||
foreach ($aList as $sLine) {
|
||||
$aData = \explode(':', $sLine, 2);
|
||||
if (!empty($aData[1]) && $sEmail === \trim($aData[0])) {
|
||||
$sEmail = \trim($aData[1]);
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ abstract class HtmlUtils
|
|||
$oBody = $oDoc->getElementsByTagName('body')->item(0);
|
||||
|
||||
$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',
|
||||
'head', 'style'
|
||||
);
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ class Http
|
|||
static::$bCache = true;
|
||||
\header('Expires: Mon, 26 Jul 1997 05:00:00 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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -617,6 +617,7 @@ class ImapClient extends \MailSo\Net\NetClient
|
|||
}
|
||||
/*
|
||||
// literal-string
|
||||
$this->hasCapability('LITERAL+')
|
||||
if (\preg_match('/[\r\n\x00\x80-\xFF]/', $sStringForEscape)) {
|
||||
return \sprintf("{%d}\r\n%s", \strlen($sStringForEscape), $sStringForEscape);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,9 +67,7 @@ class ResponseCollection extends \MailSo\Base\Collection
|
|||
}
|
||||
|
||||
if (\is_array($aList) && \count($aList)) {
|
||||
$aList = \array_map('strtoupper', $aList);
|
||||
\sort($aList);
|
||||
return $aList;
|
||||
return \array_map('strtoupper', $aList);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -316,8 +316,12 @@ abstract class SearchCriterias
|
|||
|
||||
public static function escapeSearchString(\MailSo\Imap\ImapClient $oImapClient, string $sSearch) : string
|
||||
{
|
||||
return !\MailSo\Base\Utils::IsAscii($sSearch)
|
||||
? '{'.\strlen($sSearch).'}'."\r\n".$sSearch : $oImapClient->EscapeString($sSearch);
|
||||
// https://github.com/the-djmaze/snappymail/issues/836
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class Email implements \JsonSerializable
|
|||
*/
|
||||
function __construct(string $sEmail, string $sDisplayName = '')
|
||||
{
|
||||
if (!\strlen(\trim($sEmail))) {
|
||||
if (!\strlen(\trim($sEmail)) && !\strlen(\trim($sDisplayName))) {
|
||||
throw new \InvalidArgumentException;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,12 +76,9 @@ class HeaderCollection extends \MailSo\Base\Collection
|
|||
|
||||
public function GetAsEmailCollection(string $sHeaderName, bool $bCharsetAutoDetect = false) : ?EmailCollection
|
||||
{
|
||||
$oResult = null;
|
||||
$sValue = $this->ValueByName($sHeaderName, $bCharsetAutoDetect);
|
||||
if (\strlen($sValue)) {
|
||||
$oResult = new EmailCollection($sValue);
|
||||
}
|
||||
return $oResult && $oResult->count() ? $oResult : null;
|
||||
return new EmailCollection(
|
||||
$this->ValueByName($sHeaderName, $bCharsetAutoDetect)
|
||||
);
|
||||
}
|
||||
|
||||
public function ParametersByName(string $sHeaderName) : ?ParameterCollection
|
||||
|
|
|
|||
|
|
@ -179,16 +179,19 @@ class Message extends Part
|
|||
|
||||
public function SetInReplyTo(string $sInReplyTo) : self
|
||||
{
|
||||
$this->aHeadersValue[Enumerations\Header::IN_REPLY_TO] = $sInReplyTo;
|
||||
|
||||
$sInReplyTo = \trim($sInReplyTo);
|
||||
if (\strlen($sInReplyTo)) {
|
||||
$this->aHeadersValue[Enumerations\Header::IN_REPLY_TO] = $sInReplyTo;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function SetReferences(string $sReferences) : self
|
||||
{
|
||||
$this->aHeadersValue[Enumerations\Header::REFERENCES] =
|
||||
\MailSo\Base\Utils::StripSpaces($sReferences);
|
||||
|
||||
$sReferences = \MailSo\Base\Utils::StripSpaces($sReferences);
|
||||
if (\strlen($sReferences)) {
|
||||
$this->aHeadersValue[Enumerations\Header::REFERENCES] = $sReferences;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
@ -239,8 +242,9 @@ class Message extends Part
|
|||
|
||||
public function SetTo(EmailCollection $oEmails) : self
|
||||
{
|
||||
$this->aHeadersValue[Enumerations\Header::TO_] = $oEmails;
|
||||
|
||||
if ($oEmails->count()) {
|
||||
$this->aHeadersValue[Enumerations\Header::TO_] = $oEmails;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
@ -253,22 +257,25 @@ class Message extends Part
|
|||
|
||||
public function SetReplyTo(EmailCollection $oEmails) : self
|
||||
{
|
||||
$this->aHeadersValue[Enumerations\Header::REPLY_TO] = $oEmails;
|
||||
|
||||
if ($oEmails->count()) {
|
||||
$this->aHeadersValue[Enumerations\Header::REPLY_TO] = $oEmails;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function SetCc(EmailCollection $oEmails) : self
|
||||
{
|
||||
$this->aHeadersValue[Enumerations\Header::CC] = $oEmails;
|
||||
|
||||
if ($oEmails->count()) {
|
||||
$this->aHeadersValue[Enumerations\Header::CC] = $oEmails;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function SetBcc(EmailCollection $oEmails) : self
|
||||
{
|
||||
$this->aHeadersValue[Enumerations\Header::BCC] = $oEmails;
|
||||
|
||||
if ($oEmails->count()) {
|
||||
$this->aHeadersValue[Enumerations\Header::BCC] = $oEmails;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,8 @@ abstract class NetClient
|
|||
}
|
||||
|
||||
if ($this->IsConnected()) {
|
||||
$this->writeLogException(new Exceptions\SocketAlreadyConnectedException, \LOG_ERR);
|
||||
$this->writeLogException(new Exceptions\SocketAlreadyConnectedException, \LOG_ERR, false);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->Settings = $oSettings;
|
||||
|
|
@ -214,14 +215,6 @@ abstract class NetClient
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \MailSo\Net\Exceptions\SocketConnectionDoesNotAvailableException
|
||||
*/
|
||||
public function IsConnectedWithException() : void
|
||||
{
|
||||
$this->IsConnected(true);
|
||||
}
|
||||
|
||||
public function StreamContextParams() : array
|
||||
{
|
||||
return $this->rConnect && \MailSo\Base\Utils::FunctionCallable('stream_context_get_options')
|
||||
|
|
|
|||
|
|
@ -689,6 +689,7 @@ class Actions
|
|||
'ViewImages' => 'ask',
|
||||
'ViewImagesWhitelist' => '',
|
||||
'RemoveColors' => (bool) $oConfig->Get('defaults', 'remove_colors', false),
|
||||
'AllowStyles' => false,
|
||||
'ListInlineAttachments' => false,
|
||||
'CollapseBlockquotes' => true,
|
||||
'MaxBlockquotesLevel' => 0,
|
||||
|
|
@ -801,6 +802,7 @@ class Actions
|
|||
$aResult['ViewImages'] = $oSettings->GetConf('ViewImages', $show_images ? 'always' : 'ask');
|
||||
$aResult['ViewImagesWhitelist'] = $oSettings->GetConf('ViewImagesWhitelist', '');
|
||||
$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['CollapseBlockquotes'] = (bool)$oSettings->GetConf('CollapseBlockquotes', $aResult['CollapseBlockquotes']);
|
||||
$aResult['MaxBlockquotesLevel'] = (int)$oSettings->GetConf('MaxBlockquotesLevel', $aResult['MaxBlockquotesLevel']);
|
||||
|
|
|
|||
|
|
@ -133,9 +133,8 @@ trait Contacts
|
|||
\ini_set('auto_detect_line_endings', '1');
|
||||
$mData = $this->FilesProvider()->GetFile($oAccount, $sSavedName);
|
||||
if ($mData) {
|
||||
$sFileStart = \fread($mData, 20);
|
||||
$sFileStart = \fread($mData, 128);
|
||||
\rewind($mData);
|
||||
|
||||
if (false !== $sFileStart) {
|
||||
$sFileStart = \trim($sFileStart);
|
||||
if (false !== \strpos($sFileStart, 'BEGIN:VCARD')) {
|
||||
|
|
@ -255,64 +254,45 @@ trait Contacts
|
|||
private function importContactsFromVcfFile(\RainLoop\Model\Account $oAccount, /*resource*/ $rFile): int
|
||||
{
|
||||
$iCount = 0;
|
||||
if ($oAccount && \is_resource($rFile)) {
|
||||
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
|
||||
if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) {
|
||||
$sFile = \stream_get_contents($rFile);
|
||||
if (\is_resource($rFile)) {
|
||||
\fclose($rFile);
|
||||
}
|
||||
|
||||
if (is_string($sFile) && 5 < \strlen($sFile)) {
|
||||
$this->Logger()->Write('Import contacts from vcf');
|
||||
$iCount = $oAddressBookProvider->ImportVcfFile($sFile);
|
||||
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
|
||||
if (\is_resource($rFile) && $oAddressBookProvider && $oAddressBookProvider->IsActive()) {
|
||||
try
|
||||
{
|
||||
$this->Logger()->Write('Import contacts from vcf');
|
||||
foreach (\RainLoop\Providers\AddressBook\Utils::VcfStreamToContacts($rFile) as $oContact) {
|
||||
if ($oAddressBookProvider->ContactSave($oContact)) {
|
||||
++$iCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Throwable $oExc)
|
||||
{
|
||||
$this->Logger()->WriteException($oExc);
|
||||
}
|
||||
}
|
||||
|
||||
return $iCount;
|
||||
}
|
||||
|
||||
private function importContactsFromCsvFile(\RainLoop\Model\Account $oAccount, /*resource*/ $rFile, string $sFileStart): int
|
||||
{
|
||||
$iCount = 0;
|
||||
$aHeaders = null;
|
||||
$aData = array();
|
||||
|
||||
if ($oAccount && \is_resource($rFile)) {
|
||||
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
|
||||
if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) {
|
||||
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
|
||||
if (\is_resource($rFile) && $oAddressBookProvider && $oAddressBookProvider->IsActive()) {
|
||||
try
|
||||
{
|
||||
$this->Logger()->Write('Import contacts from csv');
|
||||
$sDelimiter = ((int)\strpos($sFileStart, ',') > (int)\strpos($sFileStart, ';')) ? ',' : ';';
|
||||
|
||||
\setlocale(LC_CTYPE, 'en_US.UTF-8');
|
||||
while (false !== ($mRow = \fgetcsv($rFile, 5000, $sDelimiter, '"'))) {
|
||||
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;
|
||||
foreach (\RainLoop\Providers\AddressBook\Utils::CsvStreamToContacts($rFile, $sDelimiter) as $oContact) {
|
||||
if ($oAddressBookProvider->ContactSave($oContact)) {
|
||||
++$iCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($aData)) {
|
||||
$this->oLogger->Write('Import contacts from csv');
|
||||
$iCount = $oAddressBookProvider->ImportCsvArray($aData);
|
||||
}
|
||||
}
|
||||
catch (\Throwable $oExc)
|
||||
{
|
||||
$this->Logger()->WriteException($oExc);
|
||||
}
|
||||
}
|
||||
|
||||
return $iCount;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -989,10 +989,7 @@ trait Messages
|
|||
$oFrom = $oMessage->GetFrom();
|
||||
$oMessage->RegenerateMessageId($oFrom ? $oFrom->GetDomain() : '');
|
||||
|
||||
$oReplyTo = new \MailSo\Mime\EmailCollection($this->GetActionParam('replyTo', ''));
|
||||
if ($oReplyTo->count()) {
|
||||
$oMessage->SetReplyTo($oReplyTo);
|
||||
}
|
||||
$oMessage->SetReplyTo(new \MailSo\Mime\EmailCollection($this->GetActionParam('replyTo', '')));
|
||||
|
||||
if (!empty($this->GetActionParam('readReceiptRequest', 0))) {
|
||||
// Read Receipts Reference Main Account Email, Not Identities #147
|
||||
|
|
@ -1010,35 +1007,17 @@ trait Messages
|
|||
|
||||
$oMessage->SetSubject($this->GetActionParam('subject', ''));
|
||||
|
||||
$oToEmails = new \MailSo\Mime\EmailCollection($this->GetActionParam('to', ''));
|
||||
if ($oToEmails->count()) {
|
||||
$oMessage->SetTo($oToEmails);
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
$oMessage->SetTo(new \MailSo\Mime\EmailCollection($this->GetActionParam('to', '')));
|
||||
$oMessage->SetCc(new \MailSo\Mime\EmailCollection($this->GetActionParam('cc', '')));
|
||||
$oMessage->SetBcc(new \MailSo\Mime\EmailCollection($this->GetActionParam('bcc', '')));
|
||||
|
||||
$aDraftInfo = $this->GetActionParam('draftInfo', null);
|
||||
if ($bWithDraftInfo && \is_array($aDraftInfo) && !empty($aDraftInfo[0]) && !empty($aDraftInfo[1]) && !empty($aDraftInfo[2])) {
|
||||
$oMessage->SetDraftInfo($aDraftInfo[0], $aDraftInfo[1], $aDraftInfo[2]);
|
||||
}
|
||||
|
||||
$sInReplyTo = $this->GetActionParam('inReplyTo', '');
|
||||
if (\strlen($sInReplyTo)) {
|
||||
$oMessage->SetInReplyTo($sInReplyTo);
|
||||
}
|
||||
|
||||
$sReferences = $this->GetActionParam('references', '');
|
||||
if (\strlen($sReferences)) {
|
||||
$oMessage->SetReferences($sReferences);
|
||||
}
|
||||
$oMessage->SetInReplyTo($this->GetActionParam('inReplyTo', ''));
|
||||
$oMessage->SetReferences($this->GetActionParam('references', ''));
|
||||
|
||||
$aFoundCids = array();
|
||||
$aFoundDataURL = array();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ trait Themes
|
|||
&& ($oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount))) {
|
||||
$sTheme = (string) $oSettingsLocal->GetConf('Theme', $sTheme);
|
||||
}
|
||||
$sTheme = $this->ValidateTheme($sTheme) ?: 'Default';
|
||||
$sTheme = $this->ValidateTheme($sTheme);
|
||||
}
|
||||
return $sTheme;
|
||||
}
|
||||
|
|
@ -92,7 +92,13 @@ trait Themes
|
|||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ trait User
|
|||
$this->setSettingsFromParams($oSettings, 'ViewImages', 'string');
|
||||
$this->setSettingsFromParams($oSettings, 'ViewImagesWhitelist', 'string');
|
||||
$this->setSettingsFromParams($oSettings, 'RemoveColors', 'bool');
|
||||
$this->setSettingsFromParams($oSettings, 'AllowStyles', 'bool');
|
||||
$this->setSettingsFromParams($oSettings, 'ListInlineAttachments', 'bool');
|
||||
$this->setSettingsFromParams($oSettings, 'CollapseBlockquotes', 'bool');
|
||||
$this->setSettingsFromParams($oSettings, 'MaxBlockquotesLevel', 'int');
|
||||
|
|
|
|||
|
|
@ -6,50 +6,24 @@ use MailSo\Base\Utils;
|
|||
|
||||
class Identity implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sId;
|
||||
private string $sId;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sEmail;
|
||||
private string $sEmail;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sName;
|
||||
private string $sName = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sReplyTo;
|
||||
private string $sReplyTo = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sBcc;
|
||||
private string $sBcc = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sSignature;
|
||||
private string $sSignature = '';
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $bSignatureInsertBefore;
|
||||
private bool $bSignatureInsertBefore = false;
|
||||
|
||||
function __construct(string $sId = '', string $sEmail = '')
|
||||
{
|
||||
$this->sId = $sId;
|
||||
$this->sEmail = $sEmail;
|
||||
$this->sName = '';
|
||||
$this->sReplyTo = '';
|
||||
$this->sBcc = '';
|
||||
$this->sSignature = '';
|
||||
$this->bSignatureInsertBefore = false;
|
||||
}
|
||||
|
||||
public function Id(bool $bFillOnEmpty = false): string
|
||||
|
|
@ -84,16 +58,6 @@ class Identity implements \JsonSerializable
|
|||
return $this->sBcc;
|
||||
}
|
||||
|
||||
public function Signature(): string
|
||||
{
|
||||
return $this->sSignature;
|
||||
}
|
||||
|
||||
public function SignatureInsertBefore(): bool
|
||||
{
|
||||
return $this->bSignatureInsertBefore;
|
||||
}
|
||||
|
||||
public function SetId(string $sId): Identity
|
||||
{
|
||||
$this->sId = $sId;
|
||||
|
|
@ -151,13 +115,13 @@ class Identity implements \JsonSerializable
|
|||
public function ToSimpleJSON(): array
|
||||
{
|
||||
return array(
|
||||
'Id' => $this->Id(),
|
||||
'Email' => $this->Email(),
|
||||
'Name' => $this->Name(),
|
||||
'ReplyTo' => $this->ReplyTo(),
|
||||
'Bcc' => $this->Bcc(),
|
||||
'Signature' => $this->Signature(),
|
||||
'SignatureInsertBefore' => $this->SignatureInsertBefore()
|
||||
'Id' => $this->sId,
|
||||
'Email' => $this->sEmail,
|
||||
'Name' => $this->sName,
|
||||
'ReplyTo' => $this->sReplyTo,
|
||||
'Bcc' => $this->sBcc,
|
||||
'Signature' => $this->sSignature,
|
||||
'SignatureInsertBefore' => $this->bSignatureInsertBefore
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -165,13 +129,13 @@ class Identity implements \JsonSerializable
|
|||
public function jsonSerialize()
|
||||
{
|
||||
return array(
|
||||
'Id' => $this->Id(),
|
||||
'Email' => Utils::IdnToUtf8($this->Email()),
|
||||
'Name' => $this->Name(),
|
||||
'ReplyTo' => $this->ReplyTo(),
|
||||
'Bcc' => $this->Bcc(),
|
||||
'Signature' => $this->Signature(),
|
||||
'SignatureInsertBefore' => $this->SignatureInsertBefore()
|
||||
'Id' => $this->sId,
|
||||
'Email' => Utils::IdnToUtf8($this->sEmail),
|
||||
'Name' => $this->sName,
|
||||
'ReplyTo' => $this->sReplyTo,
|
||||
'Bcc' => $this->sBcc,
|
||||
'Signature' => $this->sSignature,
|
||||
'SignatureInsertBefore' => $this->bSignatureInsertBefore
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,37 +82,4 @@ class AddressBook extends AbstractProvider
|
|||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,11 +52,17 @@ class PdoAddressBook
|
|||
$sDsn = 'sqlite:' . APP_PRIVATE_DATA . 'AddressBook.sqlite';
|
||||
/*
|
||||
// TODO: use local db?
|
||||
$homedir = \RainLoop\Api::Actions()->StorageProvider()->GenerateFilePath(
|
||||
$oAccount,
|
||||
\RainLoop\Providers\Storage\Enumerations\StorageType::ROOT
|
||||
);
|
||||
$sDsn = 'sqlite:' . $homedir . '/AddressBook.sqlite';
|
||||
$oAccount = \RainLoop\Api::Actions()->getMainAccountFromToken(false);
|
||||
if ($oAccount) {
|
||||
$homedir = \RainLoop\Api::Actions()->StorageProvider()->GenerateFilePath(
|
||||
$oAccount,
|
||||
\RainLoop\Providers\Storage\Enumerations\StorageType::ROOT
|
||||
);
|
||||
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 {
|
||||
$sDsn = \trim($oConfig->Get('contacts', 'pdo_dsn', ''));
|
||||
|
|
@ -101,9 +107,12 @@ class PdoAddressBook
|
|||
private function prepareDatabaseSyncData() : 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(
|
||||
':id_user' => array($this->iUserID, \PDO::PARAM_INT)
|
||||
));
|
||||
$oStmt = $this->prepareAndExecute('SELECT id_contact, id_contact_str, changed, deleted, etag
|
||||
FROM rainloop_ab_contacts
|
||||
WHERE id_user = :id_user
|
||||
ORDER BY deleted DESC',
|
||||
array(':id_user' => array($this->iUserID, \PDO::PARAM_INT))
|
||||
);
|
||||
|
||||
if ($oStmt) {
|
||||
$aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
|
@ -308,6 +317,7 @@ class PdoAddressBook
|
|||
public function Export(string $sType = 'vcf') : bool
|
||||
{
|
||||
if (1 > $this->iUserID) {
|
||||
\SnappyMail\Log::warning('PdoAddressBook', 'Export() invalid $iUserID');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -317,16 +327,21 @@ class PdoAddressBook
|
|||
$aDatabaseSyncData = $this->prepareDatabaseSyncData();
|
||||
if (\count($aDatabaseSyncData)) {
|
||||
foreach ($aDatabaseSyncData as $mData) {
|
||||
if ($mData && isset($mData['id_contact'], $mData['deleted']) && !$mData['deleted']) {
|
||||
$oContact = $this->GetContactByID($mData['id_contact']);
|
||||
if ($oContact) {
|
||||
if ($rCsv) {
|
||||
Utils::VCardToCsv($rCsv, $oContact, $bCsvHeader);
|
||||
$bCsvHeader = false;
|
||||
} else {
|
||||
echo $oContact->vCard->serialize();
|
||||
try {
|
||||
// if ($mData && isset($mData['id_contact'], $mData['deleted']) && !$mData['deleted']) {
|
||||
if ($mData && !empty($mData['id_contact'])) {
|
||||
$oContact = $this->GetContactByID($mData['id_contact']);
|
||||
if ($oContact) {
|
||||
if ($rCsv) {
|
||||
Utils::VCardToCsv($rCsv, $oContact->vCard, $bCsvHeader);
|
||||
$bCsvHeader = false;
|
||||
} else {
|
||||
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
|
||||
{
|
||||
if (1 > $this->iUserID) {
|
||||
\SnappyMail\Log::warning('PdoAddressBook', 'ContactSave() invalid $iUserID');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -436,6 +452,7 @@ class PdoAddressBook
|
|||
public function DeleteContacts(array $aContactIds) : bool
|
||||
{
|
||||
if (1 > $this->iUserID) {
|
||||
\SnappyMail\Log::warning('PdoAddressBook', 'DeleteContacts() invalid $iUserID');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -744,7 +761,7 @@ class PdoAddressBook
|
|||
p.prop_value as jcard
|
||||
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)
|
||||
WHERE c.deleted = 0 AND c.id_user = :id_user';
|
||||
WHERE c.id_user = :id_user AND c.deleted = 0';
|
||||
|
||||
$aParams = array(
|
||||
':id_user' => array($this->iUserID, \PDO::PARAM_INT),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace RainLoop\Providers\AddressBook;
|
||||
|
||||
use Sabre\VObject\Component\VCard;
|
||||
|
||||
class Utils
|
||||
{
|
||||
private static $aMap = array(
|
||||
|
|
@ -22,20 +24,29 @@ class Utils
|
|||
'namesuffix' => 4,
|
||||
'shortname' => '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'),
|
||||
'businessphone2' => 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'),
|
||||
'companymainphone' => array('TEL', 'WORK'),
|
||||
|
||||
'primaryphone' => array('TEL', 'PREF,HOME'),
|
||||
'homephone' => array('TEL', 'HOME'),
|
||||
'homephone2' => 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'),
|
||||
'email2' => array('EMAIL', 'HOME'),
|
||||
'email3' => array('EMAIL', 'HOME'),
|
||||
|
|
@ -48,53 +59,73 @@ class Utils
|
|||
'emailaddress' => array('EMAIL', 'HOME'),
|
||||
'email2address' => 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'),
|
||||
'personalemail2' => array('EMAIL', 'HOME'),
|
||||
'personalemail3' => array('EMAIL', 'HOME'),
|
||||
'personalwebsite' => array('URL', 'HOME'),
|
||||
|
||||
'otheremail' => 'EMAIL',
|
||||
'otherphone' => 'TEL',
|
||||
'notes' => 'NOTE',
|
||||
'web' => 'URL',
|
||||
'businessweb' => array('URL', 'WORK'),
|
||||
'webpage' => 'URL',
|
||||
'businesswebpage' => array('URL', 'WORK'),
|
||||
'website' => 'URL',
|
||||
'businesswebsite' => array('URL', 'WORK'),
|
||||
'personalwebsite' => 'URL',
|
||||
'birthday' => 'BDAY'
|
||||
'website' => 'URL'
|
||||
/*
|
||||
TODO:
|
||||
'company' => '',
|
||||
'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();
|
||||
$iCount = 0;
|
||||
$oVCard = new \Sabre\VObject\Component\VCard;
|
||||
$oVCard = new VCard;
|
||||
$aName = ['','','','',''];
|
||||
foreach ($aItem as $sItemName => $sItemValue) {
|
||||
$sItemName = \strtoupper(\trim(\preg_replace('/[\s\-]+/', '', $sItemName)));
|
||||
foreach ($mRow as $iIndex => $sItemValue) {
|
||||
$sItemName = $aHeaders[$iIndex];
|
||||
$sItemValue = \trim($sItemValue);
|
||||
if (!empty($sItemName) && !empty($sItemValue)) {
|
||||
if (\array_key_exists($sItemName, \Sabre\VObject\Component\VCard::$propertyMap)) {
|
||||
$mData = $sItemName;
|
||||
if (isset($sItemName) && !empty($sItemValue)) {
|
||||
$mType = \is_array($sItemName) ? $sItemName[0] : $sItemName;
|
||||
++$iCount;
|
||||
if (\is_int($mType)) {
|
||||
$aName[$mType] = $sItemValue;
|
||||
} else if (\is_array($sItemName)) {
|
||||
$oVCard->add($mType, $sItemValue, ['type' => $sItemName[1]]);
|
||||
} else if ('FN' === $mType || 'NICKNAME' === $mType) {
|
||||
$oVCard->$mType = $sItemValue;
|
||||
} else {
|
||||
$sItemName = \strtolower($sItemName);
|
||||
$mData = !empty($sItemName) && isset($aMap[$sItemName]) ? $aMap[$sItemName] : null;
|
||||
}
|
||||
if ($mData) {
|
||||
$mType = \is_array($mData) ? $mData[0] : $mData;
|
||||
++$iCount;
|
||||
if (\is_int($mType)) {
|
||||
$aName[$mType] = $sItemValue;
|
||||
} else if (\is_array($mData)) {
|
||||
$oVCard->add($mType, $sItemValue, ['type' => $mData[1]]);
|
||||
} else if ('FN' === $mType || 'NICKNAME' === $mType) {
|
||||
$oVCard->$mType = $sItemValue;
|
||||
} else {
|
||||
$oVCard->add($mType, $sItemValue);
|
||||
}
|
||||
$oVCard->add($mType, $sItemValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
if ($bWithHeader) {
|
||||
|
|
@ -124,8 +155,6 @@ class Utils
|
|||
));
|
||||
}
|
||||
|
||||
$oVCard = $oContact->vCard;
|
||||
|
||||
$aName = isset($oVCard->N) ? $oVCard->N->getParts() : ['','','','',''];
|
||||
|
||||
$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);
|
||||
if ("\xef\xbb\xbf" === \substr($sVcfData, 0, 3)) {
|
||||
$sVcfData = \substr($sVcfData, 3);
|
||||
}
|
||||
$oVCardSplitter = new \Sabre\VObject\Splitter\VCard($sVcfData);
|
||||
$oVCardSplitter = new \Sabre\VObject\Splitter\VCard($rFile);
|
||||
if ($oVCardSplitter) {
|
||||
while ($oVCard = $oVCardSplitter->getNext()) {
|
||||
if ($oVCard instanceof \Sabre\VObject\Component\VCard) {
|
||||
if ($oVCard instanceof VCard) {
|
||||
\MailSo\Base\Utils::ResetTimeLimit();
|
||||
$oContact = new Classes\Contact();
|
||||
$oContact->setVCard($oVCard);
|
||||
|
|
|
|||
|
|
@ -551,7 +551,7 @@ class ServiceActions
|
|||
{
|
||||
$this->oHttp->ServerNoCache();
|
||||
$sTo = \trim($_GET['to'] ?? '');
|
||||
if (!empty($sTo) && \preg_match('/^mailto:/i', $sTo)) {
|
||||
if (\preg_match('/^mailto:/i', $sTo)) {
|
||||
\SnappyMail\Cookies::set(
|
||||
Actions::AUTH_MAILTO_TOKEN_KEY,
|
||||
Utils::EncodeKeyValuesQ(array(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class CSP
|
|||
{
|
||||
public
|
||||
$base = ["'self'"],
|
||||
$default = ["'self'"],
|
||||
$default = ["'self'", 'data:'],
|
||||
// Knockout.js requires eval() for observable binding purposes
|
||||
// Safari < 15.4 does not support strict-dynamic
|
||||
// $script = ["'strict-dynamic'", "'unsafe-eval'"],
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ abstract class Stream
|
|||
// 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"');
|
||||
// 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');
|
||||
|
||||
// Nginx: disable fastcgi_buffering and disable gzip for this request.
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class ZIP
|
|||
while ($i-- && \ob_end_clean());
|
||||
\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('Content-Transfer-Encoding: binary');
|
||||
$name = "{$name}.zip";
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ abstract class Upgrade
|
|||
new \RecursiveDirectoryIterator("{$dir}/{$folder}", \FilesystemIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
foreach ($items as $item) {
|
||||
foreach ($iterator as $item) {
|
||||
if ($item->isDir()) {
|
||||
\chmod($item, 0755);
|
||||
} else if ($item->isFile()) {
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "الرسائل في الصفحة",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Съобщения на страница",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Zpráv na stranu",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Meddelelser per side",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Entferne Hintergrund- und Textfarben aus der Nachricht",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Nachrichten pro Seite",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Nachricht als gelesen markieren nach",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Μηνύματα στη σελίδα",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Messages on page",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Messages on page",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
"MOVE_TO": "Mover",
|
||||
"NAME": "Nombre",
|
||||
"NONE": "Ninguno",
|
||||
"NOT_SPAM": "Correo no deseado",
|
||||
"NOT_SPAM": "Es correo deseado",
|
||||
"USERNAME": "Nombre de usuario",
|
||||
"PASSWORD": "Contraseña",
|
||||
"REMEMBER": "Remember",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"SAVE": "Guardar",
|
||||
"SAVE_CHANGES": "Save changes?",
|
||||
"SEARCH": "Buscar",
|
||||
"SPAM": "Es correo deseado",
|
||||
"SPAM": "Correo no deseado",
|
||||
"SUBJECT": "Asunto",
|
||||
"TEST": "Probar",
|
||||
"TO": "Para",
|
||||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Eliminar el fondo y los colores del texto del mensaje",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Mensajes en página",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Marcar el mensaje como leído después de",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Lehel kuvatavate kirjade arv",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"PREFER_HTML": "Lehenetsi HTMLa testu arruntari",
|
||||
"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",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Orrialdeko mezuak",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Markatu mezua irakurrita ondoren",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "پیامها در صفحه",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Viestiä sivulla",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -173,11 +173,11 @@
|
|||
"CONTACTS": {
|
||||
"TAB_CONTACT": "Contact",
|
||||
"TAB_LOCATIONS": "Localisations",
|
||||
"TAB_BUSINESS": "Métier",
|
||||
"TAB_BUSINESS": "Professionel",
|
||||
"LABEL_ORGANIZATION": "Entreprise",
|
||||
"LABEL_DEPARTMENT": "Department",
|
||||
"LABEL_DEPARTMENT": "Service",
|
||||
"LABEL_TITLE": "Titre",
|
||||
"TAB_CRYPTO": "Chiffre",
|
||||
"TAB_CRYPTO": "Chiffrement",
|
||||
"ASK": "Demander",
|
||||
"NEVER": "Jamais",
|
||||
"ALWAYS": "Toujours",
|
||||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"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",
|
||||
"CHECK_MAIL_INTERVAL": "Vérifier le messagerie à l'intervalle",
|
||||
"MARK_MESSAGE_READ_AFTER": "Marquer le message comme lu après",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "Images externes",
|
||||
"IMAGES_OPTION_ASK": "Toujours 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_WHITELIST": "Liste blanche Images"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Üzenetek egy oldalon",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Jelölje meg az üzenetet olvasottként",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Pesan di halaman",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Skilaboð á síðu",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"TEST": "Test",
|
||||
"UPDATE": "Aggiorna",
|
||||
"USERNAME": "Nome utente",
|
||||
"PASSWORD": "Parola d'ordine",
|
||||
"PASSWORD": "Password",
|
||||
"CANCEL": "Annulla"
|
||||
},
|
||||
"LOGIN": {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
"NONE": "Nessuno",
|
||||
"NOT_SPAM": "Segnala come non spam",
|
||||
"USERNAME": "Nome utente",
|
||||
"PASSWORD": "Parola d'ordine",
|
||||
"PASSWORD": "Password",
|
||||
"REMEMBER": "Remember",
|
||||
"REPLY_TO": "Rispondi a",
|
||||
"SAVE": "Salva",
|
||||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Rimuovi lo sfondo e i colori del testo dal messaggio",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Messaggi per pagina",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Contrassegna il messaggio come letto dopo",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"PREFER_HTML": "プレーンテキストよりHTMLを優先する",
|
||||
"PREFER_HTML_INFO": "メッセージがHTMLとプレーンテキストの両方の形式を含むことがあります。このオプションはそのような場合にどちらの形式を表示するかを制御します。",
|
||||
"REMOVE_COLORS": "本文の文字色及び背景色の設定を無視する",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "1ページに表示する件数",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "メッセージが既読となるまでの秒数",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "페이지 당 메시지 개수",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Žinučių lape",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Ziņojumi lapā",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Fjern bakgrunn og tekstfarger fra brødtekst",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Meldinger per side",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Marker melding som lest etter",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Verwijder achtergrond- en tekstkleuren uit het bericht",
|
||||
"ALLOW_STYLES": "CSS <style> toestaan",
|
||||
"MESSAGE_PER_PAGE": "Berichten op pagina",
|
||||
"CHECK_MAIL_INTERVAL": "Controleer mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Bericht markeren als gelezen na",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Wiadomości na stronę",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Mensagens por Página",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"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",
|
||||
"CHECK_MAIL_INTERVAL": "Intervalo de verificação do correio",
|
||||
"MARK_MESSAGE_READ_AFTER": "Marcar mensagem como lida após",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "Imagens externas",
|
||||
"IMAGES_OPTION_ASK": "Perguntar sempre",
|
||||
"IMAGES_OPTION_WHITELIST": "Lista Branca ou perguntar",
|
||||
"IMAGES_OPTION_DKIM": "Mostrar sempre que DKIM for válido",
|
||||
"IMAGES_OPTION_ALWAYS": "Mostrar sempre",
|
||||
"IMAGES_WHITELIST": "Lista Branca de imagens"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"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",
|
||||
"CHECK_MAIL_INTERVAL": "Intervalo de verificação do correio",
|
||||
"MARK_MESSAGE_READ_AFTER": "Marcar mensagem como lida após",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "Imagens externas",
|
||||
"IMAGES_OPTION_ASK": "Perguntar sempre",
|
||||
"IMAGES_OPTION_WHITELIST": "Lista Branca ou perguntar",
|
||||
"IMAGES_OPTION_DKIM": "Mostrar sempre que DKIM for válido",
|
||||
"IMAGES_OPTION_ALWAYS": "Mostrar sempre",
|
||||
"IMAGES_WHITELIST": "Lista Branca de imagens"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Mesaje pe o pagină",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Cообщений на одной странице",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Správ na stranu",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Sporočil na strani",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Meddelanden per sida",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Markera meddelandet som läst efter",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"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.",
|
||||
"REMOVE_COLORS": "Remove background and text colors from message body",
|
||||
"ALLOW_STYLES": "Allow <style> CSS",
|
||||
"MESSAGE_PER_PAGE": "Messages on page",
|
||||
"CHECK_MAIL_INTERVAL": "Check mail interval",
|
||||
"MARK_MESSAGE_READ_AFTER": "Mark message as read after",
|
||||
|
|
@ -421,7 +422,6 @@
|
|||
"EXTERNAL_IMAGES": "External images",
|
||||
"IMAGES_OPTION_ASK": "Always ask",
|
||||
"IMAGES_OPTION_WHITELIST": "Whitelist or ask",
|
||||
"IMAGES_OPTION_DKIM": "Show always when DKIM is valid",
|
||||
"IMAGES_OPTION_ALWAYS": "Show always",
|
||||
"IMAGES_WHITELIST": "Images whitelist"
|
||||
},
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue