mailto.ts 7.07 KB
Newer Older
liang ce committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
import { URISchemeHandler, URIComponents, URIOptions } from "../uri";
import { pctEncChar, pctDecChars, unescapeComponent } from "../uri";
import punycode from "punycode";
import { merge, subexp, toUpperCase, toArray } from "../util";

export interface MailtoHeaders {
	[hfname:string]:string
}

export interface MailtoComponents extends URIComponents {
	to:Array<string>,
	headers?:MailtoHeaders,
	subject?:string,
	body?:string
}

const O:MailtoHeaders = {};
const isIRI = true;

//RFC 3986
const UNRESERVED$$ = "[A-Za-z0-9\\-\\.\\_\\~" + (isIRI ? "\\xA0-\\u200D\\u2010-\\u2029\\u202F-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF" : "") + "]";
const HEXDIG$$ = "[0-9A-Fa-f]";  //case-insensitive
const PCT_ENCODED$ = subexp(subexp("%[EFef]" + HEXDIG$$ + "%" + HEXDIG$$ + HEXDIG$$ + "%" + HEXDIG$$ + HEXDIG$$) + "|" + subexp("%[89A-Fa-f]" + HEXDIG$$ + "%" + HEXDIG$$ + HEXDIG$$) + "|" + subexp("%" + HEXDIG$$ + HEXDIG$$));  //expanded

//RFC 5322, except these symbols as per RFC 6068: @ : / ? # [ ] & ; =
//const ATEXT$$ = "[A-Za-z0-9\\!\\#\\$\\%\\&\\'\\*\\+\\-\\/\\=\\?\\^\\_\\`\\{\\|\\}\\~]";
//const WSP$$ = "[\\x20\\x09]";
//const OBS_QTEXT$$ = "[\\x01-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]";  //(%d1-8 / %d11-12 / %d14-31 / %d127)
//const QTEXT$$ = merge("[\\x21\\x23-\\x5B\\x5D-\\x7E]", OBS_QTEXT$$);  //%d33 / %d35-91 / %d93-126 / obs-qtext
//const VCHAR$$ = "[\\x21-\\x7E]";
//const WSP$$ = "[\\x20\\x09]";
//const OBS_QP$ = subexp("\\\\" + merge("[\\x00\\x0D\\x0A]", OBS_QTEXT$$));  //%d0 / CR / LF / obs-qtext
//const FWS$ = subexp(subexp(WSP$$ + "*" + "\\x0D\\x0A") + "?" + WSP$$ + "+");
//const QUOTED_PAIR$ = subexp(subexp("\\\\" + subexp(VCHAR$$ + "|" + WSP$$)) + "|" + OBS_QP$);
//const QUOTED_STRING$ = subexp('\\"' + subexp(FWS$ + "?" + QCONTENT$) + "*" + FWS$ + "?" + '\\"');
const ATEXT$$ = "[A-Za-z0-9\\!\\$\\%\\'\\*\\+\\-\\^\\_\\`\\{\\|\\}\\~]";
const QTEXT$$ = "[\\!\\$\\%\\'\\(\\)\\*\\+\\,\\-\\.0-9\\<\\>A-Z\\x5E-\\x7E]";
const VCHAR$$ = merge(QTEXT$$, "[\\\"\\\\]");
const DOT_ATOM_TEXT$ = subexp(ATEXT$$ + "+" + subexp("\\." + ATEXT$$ + "+") + "*");
const QUOTED_PAIR$ = subexp("\\\\" + VCHAR$$);
const QCONTENT$ = subexp(QTEXT$$ + "|" + QUOTED_PAIR$);
const QUOTED_STRING$ = subexp('\\"' + QCONTENT$ + "*" + '\\"');

//RFC 6068
const DTEXT_NO_OBS$$ = "[\\x21-\\x5A\\x5E-\\x7E]";  //%d33-90 / %d94-126
const SOME_DELIMS$$ = "[\\!\\$\\'\\(\\)\\*\\+\\,\\;\\:\\@]";
const QCHAR$ = subexp(UNRESERVED$$ + "|" + PCT_ENCODED$ + "|" + SOME_DELIMS$$);
const DOMAIN$ = subexp(DOT_ATOM_TEXT$ + "|" + "\\[" + DTEXT_NO_OBS$$ + "*" + "\\]");
const LOCAL_PART$ = subexp(DOT_ATOM_TEXT$ + "|" + QUOTED_STRING$);
const ADDR_SPEC$ = subexp(LOCAL_PART$ + "\\@" + DOMAIN$);
const TO$ = subexp(ADDR_SPEC$ + subexp("\\," + ADDR_SPEC$) + "*");
const HFNAME$ = subexp(QCHAR$ + "*");
const HFVALUE$ = HFNAME$;
const HFIELD$ = subexp(HFNAME$ + "\\=" + HFVALUE$);
const HFIELDS2$ = subexp(HFIELD$ + subexp("\\&" + HFIELD$) + "*");
const HFIELDS$ = subexp("\\?" + HFIELDS2$);
const MAILTO_URI = new RegExp("^mailto\\:" + TO$ + "?" + HFIELDS$ + "?$");

const UNRESERVED = new RegExp(UNRESERVED$$, "g");
const PCT_ENCODED = new RegExp(PCT_ENCODED$, "g");
const NOT_LOCAL_PART = new RegExp(merge("[^]", ATEXT$$, "[\\.]", '[\\"]', VCHAR$$), "g");
const NOT_DOMAIN = new RegExp(merge("[^]", ATEXT$$, "[\\.]", "[\\[]", DTEXT_NO_OBS$$, "[\\]]"), "g");
const NOT_HFNAME = new RegExp(merge("[^]", UNRESERVED$$, SOME_DELIMS$$), "g");
const NOT_HFVALUE = NOT_HFNAME;
const TO = new RegExp("^" + TO$ + "$");
const HFIELDS = new RegExp("^" + HFIELDS2$ + "$");

function decodeUnreserved(str:string):string {
	const decStr = pctDecChars(str);
	return (!decStr.match(UNRESERVED) ? str : decStr);
}

const handler:URISchemeHandler<MailtoComponents> =  {
	scheme : "mailto",

	parse : function (components:URIComponents, options:URIOptions):MailtoComponents {
		const mailtoComponents = components as MailtoComponents;
		const to = mailtoComponents.to = (mailtoComponents.path ? mailtoComponents.path.split(",") : []);
		mailtoComponents.path = undefined;

		if (mailtoComponents.query) {
			let unknownHeaders = false
			const headers:MailtoHeaders = {};
			const hfields = mailtoComponents.query.split("&");

			for (let x = 0, xl = hfields.length; x < xl; ++x) {
				const hfield = hfields[x].split("=");

				switch (hfield[0]) {
					case "to":
						const toAddrs = hfield[1].split(",");
						for (let x = 0, xl = toAddrs.length; x < xl; ++x) {
							to.push(toAddrs[x]);
						}
						break;
					case "subject":
						mailtoComponents.subject = unescapeComponent(hfield[1], options);
						break;
					case "body":
						mailtoComponents.body = unescapeComponent(hfield[1], options);
						break;
					default:
						unknownHeaders = true;
						headers[unescapeComponent(hfield[0], options)] = unescapeComponent(hfield[1], options);
						break;
				}
			}

			if (unknownHeaders) mailtoComponents.headers = headers;
		}

		mailtoComponents.query = undefined;

		for (let x = 0, xl = to.length; x < xl; ++x) {
			const addr = to[x].split("@");

			addr[0] = unescapeComponent(addr[0]);

			if (!options.unicodeSupport) {
				//convert Unicode IDN -> ASCII IDN
				try {
					addr[1] = punycode.toASCII(unescapeComponent(addr[1], options).toLowerCase());
				} catch (e) {
					mailtoComponents.error = mailtoComponents.error || "Email address's domain name can not be converted to ASCII via punycode: " + e;
				}
			} else {
				addr[1] = unescapeComponent(addr[1], options).toLowerCase();
			}

			to[x] = addr.join("@");
		}

		return mailtoComponents;
	},

	serialize : function (mailtoComponents:MailtoComponents, options:URIOptions):URIComponents {
		const components = mailtoComponents as URIComponents;
		const to = toArray(mailtoComponents.to);
		if (to) {
			for (let x = 0, xl = to.length; x < xl; ++x) {
				const toAddr = String(to[x]);
				const atIdx = toAddr.lastIndexOf("@");
				const localPart = (toAddr.slice(0, atIdx)).replace(PCT_ENCODED, decodeUnreserved).replace(PCT_ENCODED, toUpperCase).replace(NOT_LOCAL_PART, pctEncChar);
				let domain = toAddr.slice(atIdx + 1);

				//convert IDN via punycode
				try {
					domain = (!options.iri ? punycode.toASCII(unescapeComponent(domain, options).toLowerCase()) : punycode.toUnicode(domain));
				} catch (e) {
					components.error = components.error || "Email address's domain name can not be converted to " + (!options.iri ? "ASCII" : "Unicode") + " via punycode: " + e;
				}

				to[x] = localPart + "@" + domain;
			}

			components.path = to.join(",");
		}

		const headers = mailtoComponents.headers = mailtoComponents.headers || {};

		if (mailtoComponents.subject) headers["subject"] = mailtoComponents.subject;
		if (mailtoComponents.body) headers["body"] = mailtoComponents.body;

		const fields = [];
		for (const name in headers) {
			if (headers[name] !== O[name]) {
				fields.push(
					name.replace(PCT_ENCODED, decodeUnreserved).replace(PCT_ENCODED, toUpperCase).replace(NOT_HFNAME, pctEncChar) +
					"=" +
					headers[name].replace(PCT_ENCODED, decodeUnreserved).replace(PCT_ENCODED, toUpperCase).replace(NOT_HFVALUE, pctEncChar)
				);
			}
		}
		if (fields.length) {
			components.query = fields.join("&");
		}

		return components;
	}
}

export default handler;