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

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

View file

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

View file

@ -1,3 +1,101 @@
## 2.26.1 2023-02-14
## Added
- Option to allow `<style>` in messages (beta)
- Message in new tab/window was missing BCC
## Changed
- Don't clone message for viewing, so that there is instant interaction with messagelist item
- emailArrayToStringLineHelper() now filters addresses without email address
- Cleanup EmailModel and better email address parsing and handling
- Workaround "server connection error"
[#936](https://github.com/the-djmaze/snappymail/pull/936)
- Cleanup AbstractViewPopup handling
- Italian translation by @lota
[#948](https://github.com/the-djmaze/snappymail/pull/948)
- Replace Element.fromHTML() with createElement()
## Fixed
- New subfolder not visible
[#937](https://github.com/the-djmaze/snappymail/pull/937)
- OpenPGP decrypted attachments `friendlySize` not a function
- Message @media print was broken due to new `display:flex`
- addressparser() didn't handle groups properly
- middleclick in messages list failed when messagesDom not yet initialized
- Cannot open messages that have an email address without an @ in the From header
[#950](https://github.com/the-djmaze/snappymail/pull/950)
- CSS don't display:flex when `[hidden]`
- Default theme handling by @Niveshkrishna
## 2.26.0 2023-02-10
## Added
- Whitelist advanced SPF/DKIM/DMARC valid feature
[#938](https://github.com/the-djmaze/snappymail/pull/938)
## Changed
- \RainLoop\Providers\AddressBook\Utils functions param `Contact` changed to `VCard`
## Fixed
- Issue with themes and Chrome cache
[#188](https://github.com/the-djmaze/snappymail/pull/188)
- Settings panel width due to display:flex
[#940](https://github.com/the-djmaze/snappymail/pull/940)
- Not respecting default theme setting
[#941](https://github.com/the-djmaze/snappymail/pull/941)
- Some files had 0755 instead of 0644
- Some spacing between message "view images" buttons for
[#201](https://github.com/the-djmaze/snappymail/pull/201)
- Whitelist failed when empty or when using `:`
[#938](https://github.com/the-djmaze/snappymail/pull/938)
- Cosmetics of the external images whitelist menu
[#939](https://github.com/the-djmaze/snappymail/pull/939)
- PdoAddressBook ORDER BY deleted DESC to prevent sync and export issues
- Undefined variable $items in upgrade.php
- qq.com not supporting literal-string in search
[#836](https://github.com/the-djmaze/snappymail/pull/836)
## 2.25.5 2023-02-09
## Added
- New dark themes by @TheCuteFoxxy
[#925](https://github.com/the-djmaze/snappymail/pull/925)
- External images option 'Always when DKIM is valid' for
[#201](https://github.com/the-djmaze/snappymail/pull/201)
- Image whitelist menu on message for
[#201](https://github.com/the-djmaze/snappymail/pull/201)
## Changed
- pt-PT translation by @ner00
[#917](https://github.com/the-djmaze/snappymail/pull/917) and
[#920](https://github.com/the-djmaze/snappymail/pull/920)
- fr-FR translation by @hguilbert
[#919](https://github.com/the-djmaze/snappymail/pull/919)
- Layout section rl-right now is `display: flex` and solves
[#928](https://github.com/the-djmaze/snappymail/pull/928)
- Some Themes background to `cover`
[#918](https://github.com/the-djmaze/snappymail/pull/918)
- Speedup cleanHtml() parser
- Reduce memory usage on addressbook import
## Fixed
- Prevent loading loop between MessageList and FolderInformation requests
- Admin -> Config layout
- Inline images sometimes failed
- Undefined index: cid
[#921](https://github.com/the-djmaze/snappymail/pull/921)
- On upgrade prevent Apache access errors for
[#358](https://github.com/the-djmaze/snappymail/pull/358)
- Import contacts as CSV is broken
[#931](https://github.com/the-djmaze/snappymail/pull/931)
### Removed
- Blockquote height calculator for
[#902](https://github.com/the-djmaze/snappymail/pull/902)
## 2.25.4 2023-02-06
## Added

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser'
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
import { 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
);
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
This app packages SnappyMail <upstream>2.25.4</upstream>.
This app packages SnappyMail <upstream>2.26.1</upstream>.
SnappyMail is a simple, modern, lightweight & fast web-based email client.

View file

@ -4,7 +4,7 @@ RUN mkdir -p /app/code
WORKDIR /app/code
# If you change the extraction below, be sure to test on scaleway
VERSION=2.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 && \

View file

@ -32,6 +32,9 @@ From that point, all instance-wide SnappyMail settings can be tweaked as you wis
![grafik](https://user-images.githubusercontent.com/63400209/199768097-7bd939a7-56d0-47ba-b481-aeac08776fb4.png)
## 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?

View file

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

View file

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

View file

View file

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,7 +29,7 @@ abstract class Stream
// We just fake Drupal https://www.drupal.org/docs/8/core/modules/big-pipe/bigpipe-environment-requirements
\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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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