|
|
@@ -0,0 +1,400 @@
|
|
|
+(function cloudflareTempEmailUtilsModule(root, factory) {
|
|
|
+ if (typeof module !== 'undefined' && module.exports) {
|
|
|
+ module.exports = factory();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ root.CloudflareTempEmailUtils = factory();
|
|
|
+})(typeof self !== 'undefined' ? self : globalThis, function createCloudflareTempEmailUtils() {
|
|
|
+ const DEFAULT_MAIL_PAGE_SIZE = 20;
|
|
|
+
|
|
|
+ function firstNonEmptyString(values) {
|
|
|
+ for (const value of values) {
|
|
|
+ if (value === undefined || value === null) continue;
|
|
|
+ const normalized = String(value).trim();
|
|
|
+ if (normalized) return normalized;
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ function normalizeCloudflareTempEmailBaseUrl(rawValue = '') {
|
|
|
+ const value = String(rawValue || '').trim();
|
|
|
+ if (!value) return '';
|
|
|
+
|
|
|
+ const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`;
|
|
|
+ try {
|
|
|
+ const parsed = new URL(candidate);
|
|
|
+ parsed.hash = '';
|
|
|
+ parsed.search = '';
|
|
|
+ const pathname = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/+$/, '');
|
|
|
+ return `${parsed.origin}${pathname}`;
|
|
|
+ } catch {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function normalizeCloudflareTempEmailDomain(rawValue = '') {
|
|
|
+ let value = String(rawValue || '').trim().toLowerCase();
|
|
|
+ if (!value) return '';
|
|
|
+ value = value.replace(/^@+/, '');
|
|
|
+ value = value.replace(/^https?:\/\//, '');
|
|
|
+ value = value.replace(/\/.*$/, '');
|
|
|
+ if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(value)) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+
|
|
|
+ function normalizeCloudflareTempEmailDomains(values) {
|
|
|
+ const domains = [];
|
|
|
+ const seen = new Set();
|
|
|
+ for (const value of Array.isArray(values) ? values : []) {
|
|
|
+ const normalized = normalizeCloudflareTempEmailDomain(value);
|
|
|
+ if (!normalized || seen.has(normalized)) continue;
|
|
|
+ seen.add(normalized);
|
|
|
+ domains.push(normalized);
|
|
|
+ }
|
|
|
+ return domains;
|
|
|
+ }
|
|
|
+
|
|
|
+ function buildCloudflareTempEmailHeaders(config = {}, options = {}) {
|
|
|
+ const headers = {};
|
|
|
+ const adminAuth = firstNonEmptyString([config.adminAuth, config.cloudflareTempEmailAdminAuth]);
|
|
|
+ const customAuth = firstNonEmptyString([config.customAuth, config.cloudflareTempEmailCustomAuth]);
|
|
|
+ if (adminAuth) {
|
|
|
+ headers['x-admin-auth'] = adminAuth;
|
|
|
+ }
|
|
|
+ if (customAuth) {
|
|
|
+ headers['x-custom-auth'] = customAuth;
|
|
|
+ }
|
|
|
+ if (options.json) {
|
|
|
+ headers['Content-Type'] = 'application/json';
|
|
|
+ }
|
|
|
+ if (options.acceptJson !== false) {
|
|
|
+ headers.Accept = 'application/json';
|
|
|
+ }
|
|
|
+ return headers;
|
|
|
+ }
|
|
|
+
|
|
|
+ function joinCloudflareTempEmailUrl(baseUrl, path) {
|
|
|
+ const normalizedBase = normalizeCloudflareTempEmailBaseUrl(baseUrl);
|
|
|
+ const normalizedPath = String(path || '').trim();
|
|
|
+ if (!normalizedBase || !normalizedPath) return normalizedBase || '';
|
|
|
+ return `${normalizedBase}${normalizedPath.startsWith('/') ? '' : '/'}${normalizedPath}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ function getCloudflareTempEmailMailRows(payload) {
|
|
|
+ if (Array.isArray(payload)) return payload;
|
|
|
+ if (!payload || typeof payload !== 'object') return [];
|
|
|
+
|
|
|
+ const candidates = [
|
|
|
+ payload.data,
|
|
|
+ payload.items,
|
|
|
+ payload.messages,
|
|
|
+ payload.mails,
|
|
|
+ payload.results,
|
|
|
+ payload.rows,
|
|
|
+ ];
|
|
|
+
|
|
|
+ for (const candidate of candidates) {
|
|
|
+ if (Array.isArray(candidate)) {
|
|
|
+ return candidate;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ function normalizeCloudflareTempEmailAddress(value) {
|
|
|
+ return String(value || '').trim().toLowerCase();
|
|
|
+ }
|
|
|
+
|
|
|
+ function splitRawMessage(raw = '') {
|
|
|
+ const source = String(raw || '');
|
|
|
+ if (!source) {
|
|
|
+ return { headerText: '', bodyText: '' };
|
|
|
+ }
|
|
|
+
|
|
|
+ const normalized = source.replace(/\r\n/g, '\n');
|
|
|
+ const separatorIndex = normalized.indexOf('\n\n');
|
|
|
+ if (separatorIndex === -1) {
|
|
|
+ return { headerText: normalized, bodyText: '' };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ headerText: normalized.slice(0, separatorIndex),
|
|
|
+ bodyText: normalized.slice(separatorIndex + 2),
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ function parseRawHeaders(headerText = '') {
|
|
|
+ const headers = {};
|
|
|
+ const lines = String(headerText || '').split('\n');
|
|
|
+ let currentName = '';
|
|
|
+
|
|
|
+ for (const line of lines) {
|
|
|
+ if (!line) continue;
|
|
|
+ if ((line.startsWith(' ') || line.startsWith('\t')) && currentName) {
|
|
|
+ headers[currentName] += ` ${line.trim()}`;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const separatorIndex = line.indexOf(':');
|
|
|
+ if (separatorIndex <= 0) continue;
|
|
|
+ currentName = line.slice(0, separatorIndex).trim().toLowerCase();
|
|
|
+ headers[currentName] = line.slice(separatorIndex + 1).trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ return headers;
|
|
|
+ }
|
|
|
+
|
|
|
+ function decodeMimeEncodedWords(value = '') {
|
|
|
+ const source = String(value || '');
|
|
|
+ return source.replace(/=\?([^?]+)\?([bBqQ])\?([^?]+)\?=/g, (_match, charset, encoding, encodedText) => {
|
|
|
+ try {
|
|
|
+ if (String(encoding).toUpperCase() === 'B') {
|
|
|
+ return decodeBytesToString(base64ToBytes(encodedText), charset);
|
|
|
+ }
|
|
|
+ return decodeBytesToString(
|
|
|
+ quotedPrintableToBytes(String(encodedText).replace(/_/g, ' '), { headerMode: true }),
|
|
|
+ charset
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ return encodedText;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function base64ToBytes(value = '') {
|
|
|
+ const normalized = String(value || '').replace(/\s+/g, '');
|
|
|
+ if (!normalized) return new Uint8Array();
|
|
|
+
|
|
|
+ if (typeof atob === 'function') {
|
|
|
+ const decoded = atob(normalized);
|
|
|
+ const bytes = new Uint8Array(decoded.length);
|
|
|
+ for (let i = 0; i < decoded.length; i += 1) {
|
|
|
+ bytes[i] = decoded.charCodeAt(i);
|
|
|
+ }
|
|
|
+ return bytes;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof Buffer !== 'undefined') {
|
|
|
+ return Uint8Array.from(Buffer.from(normalized, 'base64'));
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new Error('No base64 decoder available');
|
|
|
+ }
|
|
|
+
|
|
|
+ function quotedPrintableToBytes(value = '', options = {}) {
|
|
|
+ const { headerMode = false } = options;
|
|
|
+ const source = String(value || '')
|
|
|
+ .replace(/=\r?\n/g, '')
|
|
|
+ .replace(headerMode ? /_/g : /$^/, ' ');
|
|
|
+
|
|
|
+ const bytes = [];
|
|
|
+ for (let index = 0; index < source.length; index += 1) {
|
|
|
+ const char = source[index];
|
|
|
+ if (char === '=' && /^[0-9A-Fa-f]{2}$/.test(source.slice(index + 1, index + 3))) {
|
|
|
+ bytes.push(parseInt(source.slice(index + 1, index + 3), 16));
|
|
|
+ index += 2;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ bytes.push(char.charCodeAt(0));
|
|
|
+ }
|
|
|
+ return Uint8Array.from(bytes);
|
|
|
+ }
|
|
|
+
|
|
|
+ function decodeBytesToString(bytes, charset = 'utf-8') {
|
|
|
+ const normalizedCharset = String(charset || 'utf-8').trim().toLowerCase();
|
|
|
+ const candidates = [normalizedCharset];
|
|
|
+ if (normalizedCharset === 'utf8') {
|
|
|
+ candidates.unshift('utf-8');
|
|
|
+ }
|
|
|
+ if (normalizedCharset === 'gb2312' || normalizedCharset === 'gbk') {
|
|
|
+ candidates.unshift('gb18030');
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const candidate of candidates) {
|
|
|
+ try {
|
|
|
+ if (typeof TextDecoder !== 'undefined') {
|
|
|
+ return new TextDecoder(candidate, { fatal: false }).decode(bytes);
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ // ignore and try fallback
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof Buffer !== 'undefined') {
|
|
|
+ return Buffer.from(bytes).toString('utf8');
|
|
|
+ }
|
|
|
+
|
|
|
+ let result = '';
|
|
|
+ for (const byte of bytes) {
|
|
|
+ result += String.fromCharCode(byte);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ function getCharsetFromContentType(contentType = '') {
|
|
|
+ const match = String(contentType || '').match(/charset="?([^";]+)"?/i);
|
|
|
+ return match ? match[1].trim() : 'utf-8';
|
|
|
+ }
|
|
|
+
|
|
|
+ function getBoundaryFromContentType(contentType = '') {
|
|
|
+ const match = String(contentType || '').match(/boundary="?([^";]+)"?/i);
|
|
|
+ return match ? match[1] : '';
|
|
|
+ }
|
|
|
+
|
|
|
+ function stripHtmlTags(value = '') {
|
|
|
+ return String(value || '')
|
|
|
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
|
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
|
+ .replace(/<[^>]+>/g, ' ')
|
|
|
+ .replace(/ /gi, ' ')
|
|
|
+ .replace(/&/gi, '&')
|
|
|
+ .replace(/</gi, '<')
|
|
|
+ .replace(/>/gi, '>')
|
|
|
+ .replace(/\s+/g, ' ')
|
|
|
+ .trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ function decodeMimeBody(bodyText = '', headers = {}) {
|
|
|
+ const contentType = String(headers['content-type'] || '');
|
|
|
+ const transferEncoding = String(headers['content-transfer-encoding'] || '').trim().toLowerCase();
|
|
|
+ const charset = getCharsetFromContentType(contentType);
|
|
|
+ let decoded = String(bodyText || '');
|
|
|
+
|
|
|
+ if (transferEncoding === 'base64') {
|
|
|
+ decoded = decodeBytesToString(base64ToBytes(decoded), charset);
|
|
|
+ } else if (transferEncoding === 'quoted-printable') {
|
|
|
+ decoded = decodeBytesToString(quotedPrintableToBytes(decoded), charset);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (/text\/html/i.test(contentType)) {
|
|
|
+ return stripHtmlTags(decoded);
|
|
|
+ }
|
|
|
+ return decoded.replace(/\s+/g, ' ').trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ function extractTextFromMime(rawMessage = '', depth = 0) {
|
|
|
+ const { headerText, bodyText } = splitRawMessage(rawMessage);
|
|
|
+ const headers = parseRawHeaders(headerText);
|
|
|
+ const contentType = String(headers['content-type'] || '');
|
|
|
+ const boundary = getBoundaryFromContentType(contentType);
|
|
|
+
|
|
|
+ if (/multipart\//i.test(contentType) && boundary && depth < 6) {
|
|
|
+ const marker = `--${boundary}`;
|
|
|
+ const sections = String(bodyText || '')
|
|
|
+ .split(marker)
|
|
|
+ .map((part) => part.trim())
|
|
|
+ .filter((part) => part && part !== '--');
|
|
|
+
|
|
|
+ const extractedParts = sections
|
|
|
+ .map((part) => part.replace(/--\s*$/, '').trim())
|
|
|
+ .map((part) => extractTextFromMime(part, depth + 1)?.text || '')
|
|
|
+ .filter(Boolean);
|
|
|
+
|
|
|
+ const plainText = extractedParts.join(' ').replace(/\s+/g, ' ').trim();
|
|
|
+ return {
|
|
|
+ headers,
|
|
|
+ text: plainText,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ headers,
|
|
|
+ text: decodeMimeBody(bodyText, headers),
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ function normalizeReceivedDateTime(value) {
|
|
|
+ if (!value && value !== 0) return '';
|
|
|
+ if (typeof value === 'number' && Number.isFinite(value)) {
|
|
|
+ return new Date(value).toISOString();
|
|
|
+ }
|
|
|
+ const source = String(value || '').trim();
|
|
|
+ if (!source) return '';
|
|
|
+ const parsed = Date.parse(source);
|
|
|
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : source;
|
|
|
+ }
|
|
|
+
|
|
|
+ function normalizeCloudflareTempEmailMessage(row = {}) {
|
|
|
+ if (!row || typeof row !== 'object') return null;
|
|
|
+
|
|
|
+ const address = normalizeCloudflareTempEmailAddress(firstNonEmptyString([
|
|
|
+ row.address,
|
|
|
+ row.mail_address,
|
|
|
+ row.email,
|
|
|
+ row.recipient,
|
|
|
+ ]));
|
|
|
+ const raw = firstNonEmptyString([row.raw, row.source, row.mime, row.message]);
|
|
|
+ const parsedMime = raw ? extractTextFromMime(raw) : { headers: {}, text: '' };
|
|
|
+ const subject = decodeMimeEncodedWords(firstNonEmptyString([
|
|
|
+ row.subject,
|
|
|
+ parsedMime.headers.subject,
|
|
|
+ ]));
|
|
|
+ const fromAddress = decodeMimeEncodedWords(firstNonEmptyString([
|
|
|
+ row.from,
|
|
|
+ row.sender,
|
|
|
+ row.mail_from,
|
|
|
+ parsedMime.headers.from,
|
|
|
+ ]));
|
|
|
+ const bodyPreview = firstNonEmptyString([
|
|
|
+ row.text,
|
|
|
+ row.preview,
|
|
|
+ row.body,
|
|
|
+ parsedMime.text,
|
|
|
+ raw,
|
|
|
+ ]).replace(/\s+/g, ' ').trim();
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: firstNonEmptyString([row.id, row.mail_id]),
|
|
|
+ address,
|
|
|
+ addressId: firstNonEmptyString([row.address_id, row.addressId]),
|
|
|
+ subject,
|
|
|
+ from: {
|
|
|
+ emailAddress: {
|
|
|
+ address: fromAddress,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ bodyPreview,
|
|
|
+ raw,
|
|
|
+ receivedDateTime: normalizeReceivedDateTime(firstNonEmptyString([
|
|
|
+ row.receivedDateTime,
|
|
|
+ row.received_at,
|
|
|
+ row.created_at,
|
|
|
+ row.createdAt,
|
|
|
+ row.updated_at,
|
|
|
+ row.date,
|
|
|
+ ])),
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ function normalizeCloudflareTempEmailMailApiMessages(payload) {
|
|
|
+ return getCloudflareTempEmailMailRows(payload)
|
|
|
+ .map((row) => normalizeCloudflareTempEmailMessage(row))
|
|
|
+ .filter(Boolean);
|
|
|
+ }
|
|
|
+
|
|
|
+ function getCloudflareTempEmailAddressFromResponse(payload = {}) {
|
|
|
+ return firstNonEmptyString([
|
|
|
+ payload.address,
|
|
|
+ payload.email,
|
|
|
+ payload?.data?.address,
|
|
|
+ payload?.data?.email,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ DEFAULT_MAIL_PAGE_SIZE,
|
|
|
+ buildCloudflareTempEmailHeaders,
|
|
|
+ getCloudflareTempEmailAddressFromResponse,
|
|
|
+ joinCloudflareTempEmailUrl,
|
|
|
+ normalizeCloudflareTempEmailAddress,
|
|
|
+ normalizeCloudflareTempEmailBaseUrl,
|
|
|
+ normalizeCloudflareTempEmailDomain,
|
|
|
+ normalizeCloudflareTempEmailDomains,
|
|
|
+ normalizeCloudflareTempEmailMailApiMessages,
|
|
|
+ normalizeCloudflareTempEmailMessage,
|
|
|
+ };
|
|
|
+});
|