cloudflare-temp-email-utils.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. (function cloudflareTempEmailUtilsModule(root, factory) {
  2. if (typeof module !== 'undefined' && module.exports) {
  3. module.exports = factory();
  4. return;
  5. }
  6. root.CloudflareTempEmailUtils = factory();
  7. })(typeof self !== 'undefined' ? self : globalThis, function createCloudflareTempEmailUtils() {
  8. const DEFAULT_MAIL_PAGE_SIZE = 20;
  9. function firstNonEmptyString(values) {
  10. for (const value of values) {
  11. if (value === undefined || value === null) continue;
  12. const normalized = String(value).trim();
  13. if (normalized) return normalized;
  14. }
  15. return '';
  16. }
  17. function normalizeCloudflareTempEmailBaseUrl(rawValue = '') {
  18. const value = String(rawValue || '').trim();
  19. if (!value) return '';
  20. const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`;
  21. try {
  22. const parsed = new URL(candidate);
  23. parsed.hash = '';
  24. parsed.search = '';
  25. const pathname = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/+$/, '');
  26. return `${parsed.origin}${pathname}`;
  27. } catch {
  28. return '';
  29. }
  30. }
  31. function normalizeCloudflareTempEmailDomain(rawValue = '') {
  32. let value = String(rawValue || '').trim().toLowerCase();
  33. if (!value) return '';
  34. value = value.replace(/^@+/, '');
  35. value = value.replace(/^https?:\/\//, '');
  36. value = value.replace(/\/.*$/, '');
  37. if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(value)) {
  38. return '';
  39. }
  40. return value;
  41. }
  42. function normalizeCloudflareTempEmailDomains(values) {
  43. const domains = [];
  44. const seen = new Set();
  45. for (const value of Array.isArray(values) ? values : []) {
  46. const normalized = normalizeCloudflareTempEmailDomain(value);
  47. if (!normalized || seen.has(normalized)) continue;
  48. seen.add(normalized);
  49. domains.push(normalized);
  50. }
  51. return domains;
  52. }
  53. function buildCloudflareTempEmailHeaders(config = {}, options = {}) {
  54. const headers = {};
  55. const adminAuth = firstNonEmptyString([config.adminAuth, config.cloudflareTempEmailAdminAuth]);
  56. const customAuth = firstNonEmptyString([config.customAuth, config.cloudflareTempEmailCustomAuth]);
  57. if (adminAuth) {
  58. headers['x-admin-auth'] = adminAuth;
  59. }
  60. if (customAuth) {
  61. headers['x-custom-auth'] = customAuth;
  62. }
  63. if (options.json) {
  64. headers['Content-Type'] = 'application/json';
  65. }
  66. if (options.acceptJson !== false) {
  67. headers.Accept = 'application/json';
  68. }
  69. return headers;
  70. }
  71. function joinCloudflareTempEmailUrl(baseUrl, path) {
  72. const normalizedBase = normalizeCloudflareTempEmailBaseUrl(baseUrl);
  73. const normalizedPath = String(path || '').trim();
  74. if (!normalizedBase || !normalizedPath) return normalizedBase || '';
  75. return `${normalizedBase}${normalizedPath.startsWith('/') ? '' : '/'}${normalizedPath}`;
  76. }
  77. function getCloudflareTempEmailMailRows(payload) {
  78. if (Array.isArray(payload)) return payload;
  79. if (!payload || typeof payload !== 'object') return [];
  80. const candidates = [
  81. payload.data,
  82. payload.items,
  83. payload.messages,
  84. payload.mails,
  85. payload.results,
  86. payload.rows,
  87. ];
  88. for (const candidate of candidates) {
  89. if (Array.isArray(candidate)) {
  90. return candidate;
  91. }
  92. }
  93. return [];
  94. }
  95. function normalizeCloudflareTempEmailAddress(value) {
  96. return String(value || '').trim().toLowerCase();
  97. }
  98. function splitRawMessage(raw = '') {
  99. const source = String(raw || '');
  100. if (!source) {
  101. return { headerText: '', bodyText: '' };
  102. }
  103. const normalized = source.replace(/\r\n/g, '\n');
  104. const separatorIndex = normalized.indexOf('\n\n');
  105. if (separatorIndex === -1) {
  106. return { headerText: normalized, bodyText: '' };
  107. }
  108. return {
  109. headerText: normalized.slice(0, separatorIndex),
  110. bodyText: normalized.slice(separatorIndex + 2),
  111. };
  112. }
  113. function parseRawHeaders(headerText = '') {
  114. const headers = {};
  115. const lines = String(headerText || '').split('\n');
  116. let currentName = '';
  117. for (const line of lines) {
  118. if (!line) continue;
  119. if ((line.startsWith(' ') || line.startsWith('\t')) && currentName) {
  120. headers[currentName] += ` ${line.trim()}`;
  121. continue;
  122. }
  123. const separatorIndex = line.indexOf(':');
  124. if (separatorIndex <= 0) continue;
  125. currentName = line.slice(0, separatorIndex).trim().toLowerCase();
  126. headers[currentName] = line.slice(separatorIndex + 1).trim();
  127. }
  128. return headers;
  129. }
  130. function decodeMimeEncodedWords(value = '') {
  131. const source = String(value || '');
  132. return source.replace(/=\?([^?]+)\?([bBqQ])\?([^?]+)\?=/g, (_match, charset, encoding, encodedText) => {
  133. try {
  134. if (String(encoding).toUpperCase() === 'B') {
  135. return decodeBytesToString(base64ToBytes(encodedText), charset);
  136. }
  137. return decodeBytesToString(
  138. quotedPrintableToBytes(String(encodedText).replace(/_/g, ' '), { headerMode: true }),
  139. charset
  140. );
  141. } catch {
  142. return encodedText;
  143. }
  144. });
  145. }
  146. function base64ToBytes(value = '') {
  147. const normalized = String(value || '').replace(/\s+/g, '');
  148. if (!normalized) return new Uint8Array();
  149. if (typeof atob === 'function') {
  150. const decoded = atob(normalized);
  151. const bytes = new Uint8Array(decoded.length);
  152. for (let i = 0; i < decoded.length; i += 1) {
  153. bytes[i] = decoded.charCodeAt(i);
  154. }
  155. return bytes;
  156. }
  157. if (typeof Buffer !== 'undefined') {
  158. return Uint8Array.from(Buffer.from(normalized, 'base64'));
  159. }
  160. throw new Error('No base64 decoder available');
  161. }
  162. function quotedPrintableToBytes(value = '', options = {}) {
  163. const { headerMode = false } = options;
  164. const source = String(value || '')
  165. .replace(/=\r?\n/g, '')
  166. .replace(headerMode ? /_/g : /$^/, ' ');
  167. const bytes = [];
  168. for (let index = 0; index < source.length; index += 1) {
  169. const char = source[index];
  170. if (char === '=' && /^[0-9A-Fa-f]{2}$/.test(source.slice(index + 1, index + 3))) {
  171. bytes.push(parseInt(source.slice(index + 1, index + 3), 16));
  172. index += 2;
  173. continue;
  174. }
  175. bytes.push(char.charCodeAt(0));
  176. }
  177. return Uint8Array.from(bytes);
  178. }
  179. function decodeBytesToString(bytes, charset = 'utf-8') {
  180. const normalizedCharset = String(charset || 'utf-8').trim().toLowerCase();
  181. const candidates = [normalizedCharset];
  182. if (normalizedCharset === 'utf8') {
  183. candidates.unshift('utf-8');
  184. }
  185. if (normalizedCharset === 'gb2312' || normalizedCharset === 'gbk') {
  186. candidates.unshift('gb18030');
  187. }
  188. for (const candidate of candidates) {
  189. try {
  190. if (typeof TextDecoder !== 'undefined') {
  191. return new TextDecoder(candidate, { fatal: false }).decode(bytes);
  192. }
  193. } catch {
  194. // ignore and try fallback
  195. }
  196. }
  197. if (typeof Buffer !== 'undefined') {
  198. return Buffer.from(bytes).toString('utf8');
  199. }
  200. let result = '';
  201. for (const byte of bytes) {
  202. result += String.fromCharCode(byte);
  203. }
  204. return result;
  205. }
  206. function getCharsetFromContentType(contentType = '') {
  207. const match = String(contentType || '').match(/charset="?([^";]+)"?/i);
  208. return match ? match[1].trim() : 'utf-8';
  209. }
  210. function getBoundaryFromContentType(contentType = '') {
  211. const match = String(contentType || '').match(/boundary="?([^";]+)"?/i);
  212. return match ? match[1] : '';
  213. }
  214. function stripHtmlTags(value = '') {
  215. return String(value || '')
  216. .replace(/<style[\s\S]*?<\/style>/gi, ' ')
  217. .replace(/<script[\s\S]*?<\/script>/gi, ' ')
  218. .replace(/<[^>]+>/g, ' ')
  219. .replace(/&nbsp;/gi, ' ')
  220. .replace(/&amp;/gi, '&')
  221. .replace(/&lt;/gi, '<')
  222. .replace(/&gt;/gi, '>')
  223. .replace(/\s+/g, ' ')
  224. .trim();
  225. }
  226. function decodeMimeBody(bodyText = '', headers = {}) {
  227. const contentType = String(headers['content-type'] || '');
  228. const transferEncoding = String(headers['content-transfer-encoding'] || '').trim().toLowerCase();
  229. const charset = getCharsetFromContentType(contentType);
  230. let decoded = String(bodyText || '');
  231. if (transferEncoding === 'base64') {
  232. decoded = decodeBytesToString(base64ToBytes(decoded), charset);
  233. } else if (transferEncoding === 'quoted-printable') {
  234. decoded = decodeBytesToString(quotedPrintableToBytes(decoded), charset);
  235. }
  236. if (/text\/html/i.test(contentType)) {
  237. return stripHtmlTags(decoded);
  238. }
  239. return decoded.replace(/\s+/g, ' ').trim();
  240. }
  241. function extractTextFromMime(rawMessage = '', depth = 0) {
  242. const { headerText, bodyText } = splitRawMessage(rawMessage);
  243. const headers = parseRawHeaders(headerText);
  244. const contentType = String(headers['content-type'] || '');
  245. const boundary = getBoundaryFromContentType(contentType);
  246. if (/multipart\//i.test(contentType) && boundary && depth < 6) {
  247. const marker = `--${boundary}`;
  248. const sections = String(bodyText || '')
  249. .split(marker)
  250. .map((part) => part.trim())
  251. .filter((part) => part && part !== '--');
  252. const extractedParts = sections
  253. .map((part) => part.replace(/--\s*$/, '').trim())
  254. .map((part) => extractTextFromMime(part, depth + 1)?.text || '')
  255. .filter(Boolean);
  256. const plainText = extractedParts.join(' ').replace(/\s+/g, ' ').trim();
  257. return {
  258. headers,
  259. text: plainText,
  260. };
  261. }
  262. return {
  263. headers,
  264. text: decodeMimeBody(bodyText, headers),
  265. };
  266. }
  267. function normalizeReceivedDateTime(value) {
  268. if (!value && value !== 0) return '';
  269. if (typeof value === 'number' && Number.isFinite(value)) {
  270. return new Date(value).toISOString();
  271. }
  272. const source = String(value || '').trim();
  273. if (!source) return '';
  274. const parsed = Date.parse(source);
  275. return Number.isFinite(parsed) ? new Date(parsed).toISOString() : source;
  276. }
  277. function normalizeCloudflareTempEmailMessage(row = {}) {
  278. if (!row || typeof row !== 'object') return null;
  279. const address = normalizeCloudflareTempEmailAddress(firstNonEmptyString([
  280. row.address,
  281. row.mail_address,
  282. row.email,
  283. row.recipient,
  284. ]));
  285. const raw = firstNonEmptyString([row.raw, row.source, row.mime, row.message]);
  286. const parsedMime = raw ? extractTextFromMime(raw) : { headers: {}, text: '' };
  287. const subject = decodeMimeEncodedWords(firstNonEmptyString([
  288. row.subject,
  289. parsedMime.headers.subject,
  290. ]));
  291. const fromAddress = decodeMimeEncodedWords(firstNonEmptyString([
  292. row.from,
  293. row.sender,
  294. row.mail_from,
  295. parsedMime.headers.from,
  296. ]));
  297. const bodyPreview = firstNonEmptyString([
  298. row.text,
  299. row.preview,
  300. row.body,
  301. parsedMime.text,
  302. raw,
  303. ]).replace(/\s+/g, ' ').trim();
  304. return {
  305. id: firstNonEmptyString([row.id, row.mail_id]),
  306. address,
  307. addressId: firstNonEmptyString([row.address_id, row.addressId]),
  308. subject,
  309. from: {
  310. emailAddress: {
  311. address: fromAddress,
  312. },
  313. },
  314. bodyPreview,
  315. raw,
  316. receivedDateTime: normalizeReceivedDateTime(firstNonEmptyString([
  317. row.receivedDateTime,
  318. row.received_at,
  319. row.created_at,
  320. row.createdAt,
  321. row.updated_at,
  322. row.date,
  323. ])),
  324. };
  325. }
  326. function normalizeCloudflareTempEmailMailApiMessages(payload) {
  327. return getCloudflareTempEmailMailRows(payload)
  328. .map((row) => normalizeCloudflareTempEmailMessage(row))
  329. .filter(Boolean);
  330. }
  331. function getCloudflareTempEmailAddressFromResponse(payload = {}) {
  332. return firstNonEmptyString([
  333. payload.address,
  334. payload.email,
  335. payload?.data?.address,
  336. payload?.data?.email,
  337. ]);
  338. }
  339. return {
  340. DEFAULT_MAIL_PAGE_SIZE,
  341. buildCloudflareTempEmailHeaders,
  342. getCloudflareTempEmailAddressFromResponse,
  343. joinCloudflareTempEmailUrl,
  344. normalizeCloudflareTempEmailAddress,
  345. normalizeCloudflareTempEmailBaseUrl,
  346. normalizeCloudflareTempEmailDomain,
  347. normalizeCloudflareTempEmailDomains,
  348. normalizeCloudflareTempEmailMailApiMessages,
  349. normalizeCloudflareTempEmailMessage,
  350. };
  351. });