mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-13 03:27:39 +03:00
Improved email address parsing and handling
This commit is contained in:
parent
e8c93a1d0c
commit
33653eae81
7 changed files with 165 additions and 238 deletions
|
|
@ -4,7 +4,7 @@ import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser'
|
||||||
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
|
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
|
||||||
import { plainToHtml } from 'Common/Html';
|
import { plainToHtml } from 'Common/Html';
|
||||||
import { getNotification } from 'Common/Translator';
|
import { getNotification } from 'Common/Translator';
|
||||||
import { EmailModel } from 'Model/Email';
|
import { EmailCollectionModel } from 'Model/EmailCollection';
|
||||||
import { MessageModel } from 'Model/Message';
|
import { MessageModel } from 'Model/Message';
|
||||||
import { MessageUserStore } from 'Stores/User/Message';
|
import { MessageUserStore } from 'Stores/User/Message';
|
||||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
|
|
@ -158,27 +158,13 @@ mailToHelper = mailToUrl => {
|
||||||
const
|
const
|
||||||
email = mailToUrl[0],
|
email = mailToUrl[0],
|
||||||
params = new URLSearchParams(mailToUrl[1]),
|
params = new URLSearchParams(mailToUrl[1]),
|
||||||
toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null;
|
to = params.get('to'),
|
||||||
|
toEmailModel = value => EmailCollectionModel.fromString(value);
|
||||||
|
|
||||||
showMessageComposer([
|
showMessageComposer([
|
||||||
ComposeType.Empty,
|
ComposeType.Empty,
|
||||||
null,
|
null,
|
||||||
params.get('to')
|
toEmailModel(to ? email + ',' + to : email),
|
||||||
? Object.values(
|
|
||||||
toEmailModel(email + ',' + params.get('to')).reduce((result, value) => {
|
|
||||||
if (value) {
|
|
||||||
if (result[value.email]) {
|
|
||||||
if (!result[value.email].name) {
|
|
||||||
result[value.email] = value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result[value.email] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, {})
|
|
||||||
)
|
|
||||||
: EmailModel.parseEmailLine(email),
|
|
||||||
toEmailModel(params.get('cc')),
|
toEmailModel(params.get('cc')),
|
||||||
toEmailModel(params.get('bcc')),
|
toEmailModel(params.get('bcc')),
|
||||||
params.get('subject'),
|
params.get('subject'),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,29 @@
|
||||||
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
||||||
import { EmailModel } from 'Model/Email';
|
import { EmailModel, addressparser } from 'Model/Email';
|
||||||
|
|
||||||
const contentType = 'snappymail/emailaddress',
|
const contentType = 'snappymail/emailaddress',
|
||||||
getAddressKey = li => li?.emailaddress?.key;
|
getAddressKey = li => li?.emailaddress?.key,
|
||||||
|
|
||||||
|
parseEmailLine = line => addressparser(line).map(item =>
|
||||||
|
(item.name || item.address)
|
||||||
|
? new EmailModel(item.address, item.name) : null
|
||||||
|
).filter(v => v),
|
||||||
|
splitEmailLine = line => {
|
||||||
|
const result = [];
|
||||||
|
let exists = false;
|
||||||
|
addressparser(line).forEach(item => {
|
||||||
|
const address = (item.name || item.address)
|
||||||
|
? new EmailModel(item.address, item.name)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (address?.email) {
|
||||||
|
exists = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(address ? address.toLine() : item.name);
|
||||||
|
});
|
||||||
|
return exists ? result : null;
|
||||||
|
};
|
||||||
|
|
||||||
let dragAddress, datalist;
|
let dragAddress, datalist;
|
||||||
|
|
||||||
|
|
@ -154,8 +175,8 @@ export class EmailAddressesComponent {
|
||||||
if (val) {
|
if (val) {
|
||||||
const self = this,
|
const self = this,
|
||||||
v = val.trim(),
|
v = val.trim(),
|
||||||
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? EmailModel.splitEmailLine(val) : null,
|
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? splitEmailLine(val) : null,
|
||||||
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
|
values = (hook || [val]).map(value => parseEmailLine(value))
|
||||||
.flat(Infinity)
|
.flat(Infinity)
|
||||||
.map(item => (item.toLine ? [item.toLine(), item] : [item, null]));
|
.map(item => (item.toLine ? [item.toLine(), item] : [item, null]));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
|
|
||||||
import { ParseMime } from 'Mime/Parser';
|
import { ParseMime } from 'Mime/Parser';
|
||||||
import { AttachmentModel } from 'Model/Attachment';
|
import { AttachmentModel } from 'Model/Attachment';
|
||||||
import { EmailModel } from 'Model/Email';
|
|
||||||
import { FileInfo } from 'Common/File';
|
import { FileInfo } from 'Common/File';
|
||||||
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
|
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
|
||||||
|
|
||||||
|
|
@ -17,16 +16,10 @@ export function MimeToMessage(data, message)
|
||||||
let html = struct.getByContentType('text/html');
|
let html = struct.getByContentType('text/html');
|
||||||
html = html ? html.body : '';
|
html = html ? html.body : '';
|
||||||
|
|
||||||
if (struct.headers.subject) {
|
message.subject(struct.headerValue('subject') || '');
|
||||||
message.subject(struct.headers.subject.value);
|
|
||||||
}
|
// EmailCollectionModel
|
||||||
['from','to'].forEach(name => {
|
['from','to'].forEach(name => message[name].fromString(struct.headerValue(name)));
|
||||||
if (struct.headers[name] && !message[name].length) {
|
|
||||||
let mail = new EmailModel;
|
|
||||||
mail.parse(struct.headers[name].value);
|
|
||||||
message[name].push(mail);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
struct.forEach(part => {
|
struct.forEach(part => {
|
||||||
let cd = part.header('content-disposition'),
|
let cd = part.header('content-disposition'),
|
||||||
|
|
|
||||||
|
|
@ -18,32 +18,83 @@ import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
* @param {String} str Address field
|
* @param {String} str Address field
|
||||||
* @return {Array} An array of address objects
|
* @return {Array} An array of address objects
|
||||||
*/
|
*/
|
||||||
function addressparser(str) {
|
export function addressparser(str) {
|
||||||
var tokenizer = new Tokenizer(str);
|
str = (str || '').toString();
|
||||||
var tokens = tokenizer.tokenize();
|
|
||||||
var addresses = [];
|
|
||||||
var address = [];
|
|
||||||
var parsedAddresses = [];
|
|
||||||
|
|
||||||
tokens.forEach(token => {
|
let
|
||||||
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
|
endOperator = '',
|
||||||
address.length && addresses.push(address);
|
node = {
|
||||||
address = [];
|
type: 'text',
|
||||||
} else {
|
value: ''
|
||||||
address.push(token);
|
},
|
||||||
}
|
escaped = false,
|
||||||
});
|
address = [],
|
||||||
|
addresses = [];
|
||||||
|
|
||||||
address.length && addresses.push(address);
|
const
|
||||||
|
/*
|
||||||
addresses.forEach(address => {
|
* 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);
|
address = _handleAddress(address);
|
||||||
if (address.length) {
|
if (address.length) {
|
||||||
parsedAddresses = parsedAddresses.concat(address);
|
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 = 'address';
|
||||||
|
} else if ('(' === chr) {
|
||||||
|
node.type = 'comment';
|
||||||
|
} else if (':' === chr) {
|
||||||
|
node.type = 'group';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node.value += chr;
|
||||||
|
escaped = !escaped && '\\' === chr;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
pushToken(node);
|
||||||
|
|
||||||
return parsedAddresses;
|
pushAddress();
|
||||||
|
|
||||||
|
return addresses;
|
||||||
|
// return addresses.map(item => (item.name || item.address) ? new EmailModel(item.address, item.name) : null).filter(v => v);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -53,11 +104,11 @@ function addressparser(str) {
|
||||||
* @return {Object} Address object
|
* @return {Object} Address object
|
||||||
*/
|
*/
|
||||||
function _handleAddress(tokens) {
|
function _handleAddress(tokens) {
|
||||||
var isGroup = false;
|
let
|
||||||
var state = 'text';
|
isGroup = false,
|
||||||
var address = void 0;
|
address = {},
|
||||||
var addresses = [];
|
addresses = [],
|
||||||
var data = {
|
data = {
|
||||||
address: [],
|
address: [],
|
||||||
comment: [],
|
comment: [],
|
||||||
group: [],
|
group: [],
|
||||||
|
|
@ -66,24 +117,8 @@ function _handleAddress(tokens) {
|
||||||
|
|
||||||
// Filter out <addresses>, (comments) and regular text
|
// Filter out <addresses>, (comments) and regular text
|
||||||
tokens.forEach(token => {
|
tokens.forEach(token => {
|
||||||
if (token.type === 'operator') {
|
isGroup = isGroup || 'group' === token.type;
|
||||||
switch (token.value) {
|
data[token.type].push(token.value);
|
||||||
case '<':
|
|
||||||
state = 'address';
|
|
||||||
break;
|
|
||||||
case '(':
|
|
||||||
state = 'comment';
|
|
||||||
break;
|
|
||||||
case ':':
|
|
||||||
state = 'group';
|
|
||||||
isGroup = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
state = 'text';
|
|
||||||
}
|
|
||||||
} else if (token.value) {
|
|
||||||
data[state].push(token.value);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there is no text but a comment, replace the two
|
// If there is no text but a comment, replace the two
|
||||||
|
|
@ -94,10 +129,11 @@ function _handleAddress(tokens) {
|
||||||
|
|
||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
||||||
data.text = data.text.join(' ');
|
|
||||||
addresses.push({
|
addresses.push({
|
||||||
name: data.text || address && address.name,
|
address: '',
|
||||||
group: data.group.length ? addressparser(data.group.join(',')) : []
|
name: data.text.join(' ').trim(),
|
||||||
|
group: addressparser(data.group.join(','))
|
||||||
|
// ,comment: data.comment.join(' ').trim()
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If no address was found, try to detect one from regular text
|
// If no address was found, try to detect one from regular text
|
||||||
|
|
@ -128,7 +164,7 @@ function _handleAddress(tokens) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's still is no text but a comment exixts, replace the two
|
// If there's still is no text but a comment exists, replace the two
|
||||||
if (!data.text.length && data.comment.length) {
|
if (!data.text.length && data.comment.length) {
|
||||||
data.text = data.comment;
|
data.text = data.comment;
|
||||||
data.comment = [];
|
data.comment = [];
|
||||||
|
|
@ -139,133 +175,40 @@ function _handleAddress(tokens) {
|
||||||
data.text = data.text.concat(data.address.splice(1));
|
data.text = data.text.concat(data.address.splice(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Join values with spaces
|
|
||||||
data.text = data.text.join(' ');
|
|
||||||
data.address = data.address.join(' ');
|
|
||||||
|
|
||||||
if (!data.address && isGroup) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
address = {
|
address = {
|
||||||
address: data.address || data.text || '',
|
// Join values with spaces
|
||||||
name: data.text || data.address || ''
|
address: data.address.join(' ').trim(),
|
||||||
|
name: data.text.join(' ').trim()
|
||||||
|
// ,comment: data.comment.join(' ').trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (address.address === address.name) {
|
if (address.address === address.name) {
|
||||||
if ((address.address || '').match(/@/)) {
|
if (address.address.includes('@')) {
|
||||||
address.name = '';
|
address.name = '';
|
||||||
} else {
|
} else {
|
||||||
address.address = '';
|
address.address = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// address.address = address.address.replace(/^[<]+(.*)[>]+$/g, '$1');
|
||||||
|
|
||||||
addresses.push(address);
|
addresses.push(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
return addresses;
|
return addresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Operator tokens and which tokens are expected to end the sequence
|
|
||||||
*/
|
|
||||||
var OPERATORS = {
|
|
||||||
'"': '"',
|
|
||||||
'(': ')',
|
|
||||||
'<': '>',
|
|
||||||
',': '',
|
|
||||||
// Groups are ended by semicolons
|
|
||||||
':': ';',
|
|
||||||
// Semicolons are not a legal delimiter per the RFC2822 grammar other
|
|
||||||
// than for terminating a group, but they are also not valid for any
|
|
||||||
// other use in this context. Given that some mail clients have
|
|
||||||
// historically allowed the semicolon as a delimiter equivalent to the
|
|
||||||
// comma in their UI, it makes sense to treat them the same as a comma
|
|
||||||
// when used outside of a group.
|
|
||||||
';': ''
|
|
||||||
};
|
|
||||||
|
|
||||||
class Tokenizer
|
|
||||||
{
|
|
||||||
constructor(str) {
|
|
||||||
this.str = (str || '').toString();
|
|
||||||
this.operatorCurrent = '';
|
|
||||||
this.operatorExpecting = '';
|
|
||||||
this.node = null;
|
|
||||||
this.escaped = false;
|
|
||||||
this.list = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenize() {
|
|
||||||
var list = [];
|
|
||||||
[...this.str].forEach(c => this.checkChar(c));
|
|
||||||
|
|
||||||
this.list.forEach(node => {
|
|
||||||
node.value = (node.value || '').toString().trim();
|
|
||||||
node.value && list.push(node);
|
|
||||||
});
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkChar(chr) {
|
|
||||||
if ((chr in OPERATORS || chr === '\\') && this.escaped) {
|
|
||||||
this.escaped = false;
|
|
||||||
} else if (this.operatorExpecting && chr === this.operatorExpecting) {
|
|
||||||
this.node = {
|
|
||||||
type: 'operator',
|
|
||||||
value: chr
|
|
||||||
};
|
|
||||||
this.list.push(this.node);
|
|
||||||
this.node = null;
|
|
||||||
this.operatorExpecting = '';
|
|
||||||
this.escaped = false;
|
|
||||||
return;
|
|
||||||
} else if (!this.operatorExpecting && chr in OPERATORS) {
|
|
||||||
this.node = {
|
|
||||||
type: 'operator',
|
|
||||||
value: chr
|
|
||||||
};
|
|
||||||
this.list.push(this.node);
|
|
||||||
this.node = null;
|
|
||||||
this.operatorExpecting = OPERATORS[chr];
|
|
||||||
this.escaped = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.escaped && chr === '\\') {
|
|
||||||
this.escaped = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.node) {
|
|
||||||
this.node = {
|
|
||||||
type: 'text',
|
|
||||||
value: ''
|
|
||||||
};
|
|
||||||
this.list.push(this.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.escaped && chr !== '\\') {
|
|
||||||
this.node.value += '\\';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.node.value += chr;
|
|
||||||
this.escaped = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EmailModel extends AbstractModel {
|
export class EmailModel extends AbstractModel {
|
||||||
/**
|
/**
|
||||||
* @param {string=} email = ''
|
* @param {string=} email = ''
|
||||||
* @param {string=} name = ''
|
* @param {string=} name = ''
|
||||||
* @param {string=} dkimStatus = 'none'
|
* @param {string=} dkimStatus = 'none'
|
||||||
*/
|
*/
|
||||||
constructor(email = '', name = '', dkimStatus = 'none') {
|
constructor(email, name, dkimStatus = 'none') {
|
||||||
super();
|
super();
|
||||||
this.email = email;
|
this.email = email || '';
|
||||||
this.name = name;
|
this.name = name || '';
|
||||||
this.dkimStatus = dkimStatus;
|
this.dkimStatus = dkimStatus;
|
||||||
|
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,7 +220,7 @@ export class EmailModel extends AbstractModel {
|
||||||
static reviveFromJson(json) {
|
static reviveFromJson(json) {
|
||||||
const email = super.reviveFromJson(json);
|
const email = super.reviveFromJson(json);
|
||||||
email?.cleanup();
|
email?.cleanup();
|
||||||
return email;
|
return email?.validate() ? email : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -341,45 +284,4 @@ export class EmailModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
return result || name;
|
return result || name;
|
||||||
}
|
}
|
||||||
|
|
||||||
static splitEmailLine(line) {
|
|
||||||
const result = [];
|
|
||||||
let exists = false;
|
|
||||||
addressparser(line).forEach(item => {
|
|
||||||
const address = item.address
|
|
||||||
? new EmailModel(item.address.replace(/^[<]+(.*)[>]+$/g, '$1'), item.name || '')
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (address?.email) {
|
|
||||||
exists = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(address ? address.toLine() : item.name);
|
|
||||||
});
|
|
||||||
return exists ? result : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseEmailLine(line) {
|
|
||||||
return addressparser(line).map(item =>
|
|
||||||
item.address ? new EmailModel(item.address.replace(/^[<]+(.*)[>]+$/g, '$1'), item.name || '') : null
|
|
||||||
).filter(v => v);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} emailAddress
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
parse(emailAddress) {
|
|
||||||
emailAddress = emailAddress.trim();
|
|
||||||
if (emailAddress) {
|
|
||||||
const result = addressparser(emailAddress);
|
|
||||||
if (result.length) {
|
|
||||||
this.name = result[0].name || '';
|
|
||||||
this.email = result[0].address || '';
|
|
||||||
this.cleanup();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
import { EmailModel } from 'Model/Email';
|
import { EmailModel, addressparser } from 'Model/Email';
|
||||||
|
import { forEachObjectValue } from 'Common/Utils';
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
@ -13,6 +14,16 @@ export class EmailCollectionModel extends AbstractCollectionModel
|
||||||
return super.reviveFromJson(items, email => EmailModel.reviveFromJson(email));
|
return super.reviveFromJson(items, email => EmailModel.reviveFromJson(email));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {EmailCollectionModel}
|
||||||
|
*/
|
||||||
|
static fromString(str) {
|
||||||
|
let list = new this();
|
||||||
|
list.fromString(str);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean=} friendlyView = false
|
* @param {boolean=} friendlyView = false
|
||||||
* @param {boolean=} wrapWithLink = false
|
* @param {boolean=} wrapWithLink = false
|
||||||
|
|
@ -21,4 +32,23 @@ export class EmailCollectionModel extends AbstractCollectionModel
|
||||||
toString(friendlyView, wrapWithLink) {
|
toString(friendlyView, wrapWithLink) {
|
||||||
return this.map(email => email.toLine(friendlyView, wrapWithLink)).join(', ');
|
return this.map(email => email.toLine(friendlyView, wrapWithLink)).join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
*/
|
||||||
|
fromString(str) {
|
||||||
|
if (str) {
|
||||||
|
let items = {}, key;
|
||||||
|
addressparser(str).forEach(item => {
|
||||||
|
item = new EmailModel(item.address, 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
|
import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
|
||||||
import { EmailModel } from 'Model/Email';
|
import { EmailModel, addressparser } from 'Model/Email';
|
||||||
|
|
||||||
import { decorateKoCommands, showScreenPopup } from 'Knoin/Knoin';
|
import { decorateKoCommands, showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||||
|
|
@ -59,12 +59,7 @@ const
|
||||||
|
|
||||||
base64_encode = text => btoa(unescape(encodeURIComponent(text))).match(/.{1,76}/g).join('\r\n'),
|
base64_encode = text => btoa(unescape(encodeURIComponent(text))).match(/.{1,76}/g).join('\r\n'),
|
||||||
|
|
||||||
email = new EmailModel(),
|
getEmail = value => addressparser(value)[0]?.address || false,
|
||||||
getEmail = value => {
|
|
||||||
email.clear();
|
|
||||||
email.parse(value.trim());
|
|
||||||
return email.email || false;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Array} aList
|
* @param {Array} aList
|
||||||
|
|
|
||||||
|
|
@ -551,7 +551,7 @@ class ServiceActions
|
||||||
{
|
{
|
||||||
$this->oHttp->ServerNoCache();
|
$this->oHttp->ServerNoCache();
|
||||||
$sTo = \trim($_GET['to'] ?? '');
|
$sTo = \trim($_GET['to'] ?? '');
|
||||||
if (!empty($sTo) && \preg_match('/^mailto:/i', $sTo)) {
|
if (\preg_match('/^mailto:/i', $sTo)) {
|
||||||
\SnappyMail\Cookies::set(
|
\SnappyMail\Cookies::set(
|
||||||
Actions::AUTH_MAILTO_TOKEN_KEY,
|
Actions::AUTH_MAILTO_TOKEN_KEY,
|
||||||
Utils::EncodeKeyValuesQ(array(
|
Utils::EncodeKeyValuesQ(array(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue