emailjs/smtp/address.ts

238 lines
5.6 KiB
TypeScript
Raw Normal View History

2020-06-17 06:32:18 +00:00
interface AddressToken {
type: 'operator' | 'text';
value: string;
}
export interface AddressObject {
address?: string;
name?: string;
group?: AddressObject[];
}
2020-06-17 03:11:19 +00:00
/*
* Operator tokens and which tokens are expected to end the sequence
*/
2020-06-17 06:32:18 +00:00
const OPERATORS = new Map([
['"', '"'],
['(', ')'],
['<', '>'],
[',', ''],
2020-06-17 03:11:19 +00:00
// Groups are ended by semicolons
2020-06-17 06:32:18 +00:00
[':', ';'],
2020-06-17 03:11:19 +00:00
// 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.
2020-06-17 06:32:18 +00:00
[';', ''],
]);
2020-06-17 03:11:19 +00:00
/**
2020-06-17 06:32:18 +00:00
* Tokenizes the original input string
2020-06-17 03:11:19 +00:00
*
2020-06-17 06:32:18 +00:00
* @param {string | string[] | undefined} address string(s) to tokenize
* @return {AddressToken[]} An array of operator|text tokens
2020-06-17 03:11:19 +00:00
*/
2020-06-17 06:32:18 +00:00
function tokenizeAddress(address: string | string[] = '') {
const tokens: AddressToken[] = [];
let token: AddressToken | undefined = undefined;
let operator: string | undefined = undefined;
for (const character of address.toString()) {
if ((operator?.length ?? 0) > 0 && character === operator) {
tokens.push({ type: 'operator', value: character });
token = undefined;
operator = undefined;
} else if ((operator?.length ?? 0) === 0 && OPERATORS.has(character)) {
tokens.push({ type: 'operator', value: character });
token = undefined;
operator = OPERATORS.get(character);
} else {
if (token == null) {
token = { type: 'text', value: character };
tokens.push(token);
} else {
token.value += character;
2020-06-17 03:11:19 +00:00
}
}
}
2020-06-17 06:32:18 +00:00
return tokens
.map((x) => {
x.value = x.value.trim();
return x;
})
.filter((x) => x.value.length > 0);
2020-06-17 03:11:19 +00:00
}
2020-06-17 06:32:18 +00:00
2020-06-17 03:11:19 +00:00
/**
* Converts tokens for a single address into an address object
*
2020-06-17 06:32:18 +00:00
* @param {AddressToken[]} tokens Tokens object
2020-06-17 03:11:19 +00:00
* @return {AddressObject[]} addresses object array
*/
2020-06-17 06:32:18 +00:00
function convertAddressTokens(tokens: AddressToken[]) {
const addressObjects: AddressObject[] = [];
const groups: string[] = [];
2020-06-17 03:11:19 +00:00
let addresses: string[] = [];
let comments: string[] = [];
let texts: string[] = [];
2020-06-17 06:32:18 +00:00
let state = 'text';
let isGroup = false;
function handleToken(token: AddressToken) {
2020-06-17 03:11:19 +00:00
if (token.type === 'operator') {
switch (token.value) {
case '<':
state = 'address';
break;
case '(':
state = 'comment';
break;
case ':':
state = 'group';
isGroup = true;
break;
default:
state = 'text';
break;
}
2020-06-17 06:32:18 +00:00
} else if (token.value.length > 0) {
switch (state) {
case 'address':
addresses.push(token.value);
break;
case 'comment':
comments.push(token.value);
break;
case 'group':
groups.push(token.value);
break;
default:
texts.push(token.value);
break;
2020-06-17 03:11:19 +00:00
}
}
}
2020-06-17 06:32:18 +00:00
// Filter out <addresses>, (comments) and regular text
for (const token of tokens) {
handleToken(token);
}
2020-06-17 03:11:19 +00:00
// If there is no text but a comment, replace the two
if (texts.length === 0 && comments.length > 0) {
texts = [...comments];
comments = [];
}
2020-06-17 06:32:18 +00:00
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
2020-06-17 03:11:19 +00:00
if (isGroup) {
addressObjects.push({
2020-06-17 06:32:18 +00:00
name: texts.length === 0 ? undefined : texts.join(' '),
2020-06-17 03:11:19 +00:00
group: groups.length > 0 ? addressparser(groups.join(',')) : [],
});
} else {
// If no address was found, try to detect one from regular text
if (addresses.length === 0 && texts.length > 0) {
for (let i = texts.length - 1; i >= 0; i--) {
if (texts[i].match(/^[^@\s]+@[^@\s]+$/)) {
addresses = texts.splice(i, 1);
break;
}
}
// still no address
if (addresses.length === 0) {
for (let i = texts.length - 1; i >= 0; i--) {
texts[i] = texts[i]
2020-06-17 06:32:18 +00:00
.replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, (address: string) => {
if (addresses.length === 0) {
addresses = [address.trim()];
return ' ';
} else {
return address;
}
})
2020-06-17 03:11:19 +00:00
.trim();
2020-06-17 06:32:18 +00:00
if (addresses.length > 0) {
2020-06-17 03:11:19 +00:00
break;
}
}
}
}
// If there's still is no text but a comment exixts, replace the two
if (texts.length === 0 && comments.length > 0) {
texts = [...comments];
comments = [];
}
// Keep only the first address occurence, push others to regular text
if (addresses.length > 1) {
2020-06-17 06:32:18 +00:00
texts = [...texts, ...addresses.splice(1)];
2020-06-17 03:11:19 +00:00
}
2020-06-17 06:32:18 +00:00
if (addresses.length === 0 && isGroup) {
2020-06-17 03:11:19 +00:00
return [];
} else {
2020-06-17 06:32:18 +00:00
// Join values with spaces
let address = addresses.join(' ');
let name = texts.length === 0 ? address : texts.join(' ');
2020-06-17 03:11:19 +00:00
2020-06-17 06:32:18 +00:00
if (address === name) {
if (address.match(/@/)) {
name = '';
2020-06-17 03:11:19 +00:00
} else {
2020-06-17 06:32:18 +00:00
address = '';
2020-06-17 03:11:19 +00:00
}
}
2020-06-17 06:32:18 +00:00
addressObjects.push({ address, name });
2020-06-17 03:11:19 +00:00
}
}
return addressObjects;
}
/**
* Parses structured e-mail addresses from an address field
*
* Example:
*
* "Name <address@domain>"
*
* will be converted to
*
* [{name: "Name", address: "address@domain"}]
*
2020-06-17 06:32:18 +00:00
* @param {string | string[] | undefined} address Address field
2020-06-17 03:11:19 +00:00
* @return {AddressObject[]} An array of address objects
*/
2020-06-17 06:32:18 +00:00
export function addressparser(address?: string | string[]) {
const addresses: AddressObject[] = [];
let tokens: AddressToken[] = [];
2020-06-17 03:11:19 +00:00
2020-06-17 06:32:18 +00:00
for (const token of tokenizeAddress(address)) {
2020-06-17 03:11:19 +00:00
if (
token.type === 'operator' &&
(token.value === ',' || token.value === ';')
) {
2020-06-17 06:32:18 +00:00
if (tokens.length > 0) {
addresses.push(...convertAddressTokens(tokens));
2020-06-17 03:11:19 +00:00
}
2020-06-17 06:32:18 +00:00
tokens = [];
2020-06-17 03:11:19 +00:00
} else {
2020-06-17 06:32:18 +00:00
tokens.push(token);
2020-06-17 03:11:19 +00:00
}
}
2020-06-17 06:32:18 +00:00
if (tokens.length > 0) {
addresses.push(...convertAddressTokens(tokens));
2020-06-17 03:11:19 +00:00
}
2020-06-17 06:32:18 +00:00
return addresses;
2020-06-17 03:11:19 +00:00
}