Files
GuJumpgate/cloudflare-temp-email-utils.js

408 lines
12 KiB
JavaScript

(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(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/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 originalRecipient = normalizeCloudflareTempEmailAddress(firstNonEmptyString([
row.original_recipient,
row.originalRecipient,
row.original_recipient_email,
row.originalRecipientEmail,
]));
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,
originalRecipient,
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,
};
});