mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-12-29 11:01:34 +08:00
399 lines
8.2 KiB
JavaScript
399 lines
8.2 KiB
JavaScript
|
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
|
|
|
'use strict';
|
|
|
|
/**
|
|
* Parses structured e-mail addresses from an address field
|
|
*
|
|
* Example:
|
|
*
|
|
* "Name <address@domain>"
|
|
*
|
|
* will be converted to
|
|
*
|
|
* [{name: "Name", address: "address@domain"}]
|
|
*
|
|
* @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 = [];
|
|
|
|
tokens.forEach(token => {
|
|
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
|
|
if (address.length) {
|
|
addresses.push(address);
|
|
}
|
|
address = [];
|
|
} else {
|
|
address.push(token);
|
|
}
|
|
});
|
|
|
|
if (address.length) {
|
|
addresses.push(address);
|
|
}
|
|
|
|
addresses.forEach(address => {
|
|
address = _handleAddress(address);
|
|
if (address.length) {
|
|
parsedAddresses = parsedAddresses.concat(address);
|
|
}
|
|
});
|
|
|
|
return parsedAddresses;
|
|
}
|
|
|
|
/**
|
|
* Converts tokens for a single address into an address object
|
|
*
|
|
* @param {Array} tokens Tokens object
|
|
* @return {Object} Address object
|
|
*/
|
|
function _handleAddress(tokens) {
|
|
var isGroup = false;
|
|
var state = 'text';
|
|
var address = void 0;
|
|
var addresses = [];
|
|
var data = {
|
|
address: [],
|
|
comment: [],
|
|
group: [],
|
|
text: []
|
|
};
|
|
|
|
// Filter out <addresses>, (comments) and regular text
|
|
tokens.forEach(token => {
|
|
if (token.type === 'operator') {
|
|
switch (token.value) {
|
|
case '<':
|
|
state = 'address';
|
|
break;
|
|
case '(':
|
|
state = 'comment';
|
|
break;
|
|
case ':':
|
|
state = 'group';
|
|
isGroup = true;
|
|
break;
|
|
default:
|
|
state = 'text';
|
|
}
|
|
} else if (token.value) {
|
|
data[state].push(token.value);
|
|
}
|
|
});
|
|
|
|
// If there is no text but a comment, replace the two
|
|
if (!data.text.length && data.comment.length) {
|
|
data.text = data.comment;
|
|
data.comment = [];
|
|
}
|
|
|
|
if (isGroup) {
|
|
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
|
data.text = data.text.join(' ');
|
|
addresses.push({
|
|
name: data.text || address && address.name,
|
|
group: data.group.length ? addressparser(data.group.join(',')) : []
|
|
});
|
|
} else {
|
|
// If no address was found, try to detect one from regular text
|
|
if (!data.address.length && data.text.length) {
|
|
var i = data.text.length;
|
|
while (i--) {
|
|
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
|
|
data.address = data.text.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// still no address
|
|
if (!data.address.length) {
|
|
i = data.text.length;
|
|
while (i--) {
|
|
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, address => {
|
|
if (!data.address.length) {
|
|
data.address = [address.trim()];
|
|
return '';
|
|
}
|
|
return address.trim();
|
|
});
|
|
if (data.address.length) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there's still is no text but a comment exixts, replace the two
|
|
if (!data.text.length && data.comment.length) {
|
|
data.text = data.comment;
|
|
data.comment = [];
|
|
}
|
|
|
|
// Keep only the first address occurence, push others to regular text
|
|
if (data.address.length > 1) {
|
|
data.text = data.text.concat(data.address.splice(1));
|
|
}
|
|
|
|
// Join values with spaces
|
|
data.text = data.text.join(' ');
|
|
data.address = data.address.join(' ');
|
|
|
|
if (!data.address && isGroup) {
|
|
return [];
|
|
}
|
|
address = {
|
|
address: data.address || data.text || '',
|
|
name: data.text || data.address || ''
|
|
};
|
|
|
|
if (address.address === address.name) {
|
|
if ((address.address || '').match(/@/)) {
|
|
address.name = '';
|
|
} else {
|
|
address.address = '';
|
|
}
|
|
}
|
|
|
|
addresses.push(address);
|
|
}
|
|
|
|
return addresses;
|
|
}
|
|
|
|
/*
|
|
* 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();
|
|
if (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'
|
|
* @param {string=} dkimValue = ''
|
|
*/
|
|
constructor(email = '', name = '', dkimStatus = 'none', dkimValue = '') {
|
|
super();
|
|
this.email = email;
|
|
this.name = name;
|
|
this.dkimStatus = dkimStatus;
|
|
this.dkimValue = dkimValue;
|
|
|
|
this.clearDuplicateName();
|
|
}
|
|
|
|
/**
|
|
* @static
|
|
* @param {FetchJsonEmail} json
|
|
* @returns {?EmailModel}
|
|
*/
|
|
static reviveFromJson(json) {
|
|
const email = super.reviveFromJson(json);
|
|
email && email.clearDuplicateName();
|
|
return email;
|
|
}
|
|
|
|
/**
|
|
* @returns {void}
|
|
*/
|
|
clear() {
|
|
this.email = '';
|
|
this.name = '';
|
|
|
|
this.dkimStatus = 'none';
|
|
this.dkimValue = '';
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean}
|
|
*/
|
|
validate() {
|
|
return this.name || this.email;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} withoutName = false
|
|
* @returns {string}
|
|
*/
|
|
hash(withoutName = false) {
|
|
return '#' + (withoutName ? '' : this.name) + '#' + this.email + '#';
|
|
}
|
|
|
|
/**
|
|
* @returns {void}
|
|
*/
|
|
clearDuplicateName() {
|
|
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
|
|
* @returns {string}
|
|
*/
|
|
toLine(friendlyView) {
|
|
let result = this.email;
|
|
return (result && this.name)
|
|
? (friendlyView ? this.name : '"' + this.name + '" <' + result + '>')
|
|
: result;
|
|
}
|
|
|
|
static splitEmailLine(line) {
|
|
const parsedResult = addressparser(line);
|
|
if (parsedResult.length) {
|
|
const result = [];
|
|
let exists = false;
|
|
parsedResult.forEach((item) => {
|
|
const address = item.address
|
|
? new EmailModel(item.address.replace(/^[<]+(.*)[>]+$/g, '$1'), item.name || '')
|
|
: null;
|
|
|
|
if (address && address.email) {
|
|
exists = true;
|
|
}
|
|
|
|
result.push(address ? address.toLine() : item.name);
|
|
});
|
|
|
|
return exists ? result : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static parseEmailLine(line) {
|
|
const parsedResult = addressparser(line);
|
|
if (parsedResult.length) {
|
|
return parsedResult.map(item =>
|
|
item.address ? new EmailModel(item.address.replace(/^[<]+(.*)[>]+$/g, '$1'), item.name || '') : null
|
|
).filter(v => v);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @param {string} emailAddress
|
|
* @returns {boolean}
|
|
*/
|
|
parse(emailAddress) {
|
|
emailAddress = emailAddress.trim();
|
|
if (!emailAddress) {
|
|
return false;
|
|
}
|
|
|
|
const result = addressparser(emailAddress);
|
|
if (result.length) {
|
|
this.name = result[0].name || '';
|
|
this.email = result[0].address || '';
|
|
this.clearDuplicateName();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|