fix cloudflare temp email aws code parsing

This commit is contained in:
QLHazyCoder
2026-05-22 09:58:05 +08:00
parent 6db6725c0c
commit acf29dbd87
4 changed files with 296 additions and 20 deletions
+186 -17
View File
@@ -11,6 +11,7 @@
function firstNonEmptyString(values) { function firstNonEmptyString(values) {
for (const value of values) { for (const value of values) {
if (value === undefined || value === null) continue; if (value === undefined || value === null) continue;
if (typeof value === 'object') continue;
const normalized = String(value).trim(); const normalized = String(value).trim();
if (normalized) return normalized; if (normalized) return normalized;
} }
@@ -87,26 +88,83 @@
if (Array.isArray(payload)) return payload; if (Array.isArray(payload)) return payload;
if (!payload || typeof payload !== 'object') return []; if (!payload || typeof payload !== 'object') return [];
const candidates = [ const rowKeys = [
payload.data, 'data',
payload.items, 'items',
payload.messages, 'messages',
payload.mails, 'mails',
payload.results, 'results',
payload.rows, 'result',
'rows',
'list',
'hydra:member',
]; ];
const queue = [payload];
const seen = new Set();
for (const candidate of candidates) { while (queue.length) {
const current = queue.shift();
if (!current || typeof current !== 'object' || seen.has(current)) {
continue;
}
seen.add(current);
for (const key of rowKeys) {
const candidate = current[key];
if (Array.isArray(candidate)) { if (Array.isArray(candidate)) {
return candidate; return candidate;
} }
if (candidate && typeof candidate === 'object') {
queue.push(candidate);
}
}
} }
return []; return [];
} }
function normalizeCloudflareTempEmailAddress(value) { function normalizeCloudflareTempEmailAddress(value) {
return String(value || '').trim().toLowerCase(); const source = String(value || '').trim();
if (!source) return '';
const bracketMatch = source.match(/<\s*([^<>\s]+@[^<>\s]+)\s*>/);
const directMatch = source.match(/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/i);
return String(bracketMatch?.[1] || directMatch?.[0] || source).trim().toLowerCase();
}
function collectTextCandidates(value, keys = []) {
const results = [];
const visit = (entry) => {
if (entry === undefined || entry === null) return;
if (typeof entry === 'string' || typeof entry === 'number') {
const normalized = String(entry).trim();
if (normalized) results.push(normalized);
return;
}
if (Array.isArray(entry)) {
entry.forEach(visit);
return;
}
if (typeof entry === 'object') {
const objectKeys = keys.length ? keys : Object.keys(entry);
for (const key of objectKeys) {
if (Object.prototype.hasOwnProperty.call(entry, key)) {
visit(entry[key]);
}
}
}
};
visit(value);
return results;
}
function firstTextCandidate(values, keys = []) {
for (const value of values) {
const candidates = collectTextCandidates(value, keys);
for (const candidate of candidates) {
if (candidate) return candidate;
}
}
return '';
} }
function splitRawMessage(raw = '') { function splitRawMessage(raw = '') {
@@ -245,19 +303,53 @@
return match ? match[1] : ''; return match ? match[1] : '';
} }
function decodeHtmlEntityCode(value, radix = 10) {
const codePoint = parseInt(value, radix);
if (!Number.isFinite(codePoint) || codePoint < 0 || codePoint > 0x10FFFF) {
return ' ';
}
return String.fromCodePoint(codePoint);
}
function stripHtmlTags(value = '') { function stripHtmlTags(value = '') {
return String(value || '') return String(value || '')
.replace(/<style[\s\S]*?<\/style>/gi, ' ') .replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<script[\s\S]*?<\/script>/gi, ' ') .replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<[^>]+>/g, ' ') .replace(/<[^>]+>/g, ' ')
.replace(/&#x([0-9a-f]+);/gi, (_match, code) => decodeHtmlEntityCode(code, 16))
.replace(/&#(\d+);/g, (_match, code) => decodeHtmlEntityCode(code, 10))
.replace(/&nbsp;/gi, ' ') .replace(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&') .replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<') .replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>') .replace(/&gt;/gi, '>')
.replace(/&quot;/gi, '"')
.replace(/&#39;/g, "'")
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
.trim(); .trim();
} }
function normalizeBodyText(value = '', options = {}) {
const { html = false } = options;
const source = String(value || '').trim();
if (!source) return '';
const text = html || /<\/?[a-z][\s\S]*>/i.test(source)
? stripHtmlTags(source)
: source;
return text.replace(/\s+/g, ' ').trim();
}
function firstBodyTextCandidate(values, options = {}) {
const { keys = [], html = false } = options;
for (const value of values) {
const candidates = collectTextCandidates(value, keys);
for (const candidate of candidates) {
const normalized = normalizeBodyText(candidate, { html });
if (normalized) return normalized;
}
}
return '';
}
function decodeMimeBody(bodyText = '', headers = {}) { function decodeMimeBody(bodyText = '', headers = {}) {
const contentType = String(headers['content-type'] || ''); const contentType = String(headers['content-type'] || '');
const transferEncoding = String(headers['content-transfer-encoding'] || '').trim().toLowerCase(); const transferEncoding = String(headers['content-transfer-encoding'] || '').trim().toLowerCase();
@@ -321,40 +413,114 @@
function normalizeCloudflareTempEmailMessage(row = {}) { function normalizeCloudflareTempEmailMessage(row = {}) {
if (!row || typeof row !== 'object') return null; if (!row || typeof row !== 'object') return null;
const address = normalizeCloudflareTempEmailAddress(firstNonEmptyString([ const raw = firstTextCandidate([row.raw, row.source, row.mime, row.message], [
'raw',
'source',
'mime',
'message',
'content',
'value',
]);
const parsedMime = raw ? extractTextFromMime(raw) : { headers: {}, text: '' };
const address = normalizeCloudflareTempEmailAddress(firstTextCandidate([
row.address, row.address,
row.mail_address, row.mail_address,
row.mailAddress,
row.email, row.email,
row.recipient, row.recipient,
row.to,
row.mailTo,
row.receiver,
row.receivers,
row.envelope_to,
row.envelopeTo,
parsedMime.headers.to,
], [
'emailAddress',
'address',
'email',
'value',
'recipient',
'mailbox',
'to',
])); ]));
const originalRecipient = normalizeCloudflareTempEmailAddress(firstNonEmptyString([ const originalRecipient = normalizeCloudflareTempEmailAddress(firstTextCandidate([
row.original_recipient, row.original_recipient,
row.originalRecipient, row.originalRecipient,
row.original_recipient_email, row.original_recipient_email,
row.originalRecipientEmail, row.originalRecipientEmail,
row.envelope_to,
row.envelopeTo,
], [
'emailAddress',
'address',
'email',
'value',
'recipient',
'mailbox',
'to',
])); ]));
const raw = firstNonEmptyString([row.raw, row.source, row.mime, row.message]); const subject = decodeMimeEncodedWords(firstTextCandidate([
const parsedMime = raw ? extractTextFromMime(raw) : { headers: {}, text: '' };
const subject = decodeMimeEncodedWords(firstNonEmptyString([
row.subject, row.subject,
row.title,
parsedMime.headers.subject, parsedMime.headers.subject,
])); ]));
const fromAddress = decodeMimeEncodedWords(firstNonEmptyString([ const fromAddress = decodeMimeEncodedWords(firstTextCandidate([
row.from, row.from,
row.sender, row.sender,
row.mail_from, row.mail_from,
row.mailFrom,
row.from_email,
row.fromEmail,
row.sender_email,
row.senderEmail,
parsedMime.headers.from, parsedMime.headers.from,
], [
'emailAddress',
'address',
'email',
'value',
'name',
'from',
'sender',
])); ]));
const bodyPreview = firstNonEmptyString([ const bodyText = firstBodyTextCandidate([
row.bodyPreview,
row.snippet,
row.text, row.text,
row.text_content,
row.textContent,
row.preview, row.preview,
row.body, row.body,
row.content,
], {
keys: ['content', 'text', 'plain', 'value', 'body', 'bodyPreview'],
});
const htmlText = firstBodyTextCandidate([
row.html_content,
row.htmlContent,
row.html_body,
row.htmlBody,
row.body_html,
row.bodyHtml,
row.content_html,
row.contentHtml,
row.raw_html,
row.rawHtml,
row.html,
], {
html: true,
keys: ['content', 'html', 'value', 'body'],
});
const bodyPreview = firstNonEmptyString([
bodyText,
htmlText,
parsedMime.text, parsedMime.text,
raw, raw,
]).replace(/\s+/g, ' ').trim(); ]).replace(/\s+/g, ' ').trim();
return { return {
id: firstNonEmptyString([row.id, row.mail_id]), id: firstNonEmptyString([row.id, row._id, row.mail_id, row.mailId, row.message_id, row.messageId, row.msgid]),
address, address,
originalRecipient, originalRecipient,
addressId: firstNonEmptyString([row.address_id, row.addressId]), addressId: firstNonEmptyString([row.address_id, row.addressId]),
@@ -369,10 +535,13 @@
receivedDateTime: normalizeReceivedDateTime(firstNonEmptyString([ receivedDateTime: normalizeReceivedDateTime(firstNonEmptyString([
row.receivedDateTime, row.receivedDateTime,
row.received_at, row.received_at,
row.receivedAt,
row.created_at, row.created_at,
row.createdAt, row.createdAt,
row.updated_at, row.updated_at,
row.updatedAt,
row.date, row.date,
row.timestamp,
])), ])),
}; };
} }
+5 -1
View File
@@ -45,7 +45,9 @@
continue; continue;
} }
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, ''); const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
const match = normalizedText.match(new RegExp(source, flags)); const regex = new RegExp(source, flags);
regex.lastIndex = 0;
const match = regex.exec(normalizedText);
if (!match) { if (!match) {
continue; continue;
} }
@@ -252,6 +254,8 @@
excludeCodes: filters.excludeCodes, excludeCodes: filters.excludeCodes,
senderFilters: filters.senderFilters, senderFilters: filters.senderFilters,
subjectFilters: filters.subjectFilters, subjectFilters: filters.subjectFilters,
requiredKeywords: filters.requiredKeywords,
codePatterns: filters.codePatterns,
}); });
return { return {
+70
View File
@@ -9,6 +9,9 @@ const {
normalizeCloudflareTempEmailDomains, normalizeCloudflareTempEmailDomains,
normalizeCloudflareTempEmailMailApiMessages, normalizeCloudflareTempEmailMailApiMessages,
} = require('../cloudflare-temp-email-utils.js'); } = require('../cloudflare-temp-email-utils.js');
const {
pickVerificationMessageWithTimeFallback,
} = require('../hotmail-utils.js');
test('normalizeCloudflareTempEmailBaseUrl normalizes host and preserves path', () => { test('normalizeCloudflareTempEmailBaseUrl normalizes host and preserves path', () => {
assert.equal( assert.equal(
@@ -103,6 +106,73 @@ test('normalizeCloudflareTempEmailMailApiMessages decodes multipart quoted print
assert.equal(messages[0].subject, 'Login code'); assert.equal(messages[0].subject, 'Login code');
}); });
test('normalizeCloudflareTempEmailMailApiMessages supports nested CFTE rows with structured AWS html mail', () => {
const messages = normalizeCloudflareTempEmailMailApiMessages({
data: {
results: [
{
_id: 'aws-mail-1',
to: [{ address: 'tmpjjwk205m0h@edu.email.qlhazycoder.tech' }],
from: {
emailAddress: {
address: 'no-reply@signin.aws',
},
},
title: '验证您的 AWS 构建者 ID 电子邮件地址',
body_html: [
'<html><body>',
'<p>验证码</p>',
'<p style="background-color:#F3F3F3">248680</p>',
'</body></html>',
].join(''),
createdAt: '2026-05-22T09:41:00.000Z',
},
],
},
});
assert.equal(messages.length, 1);
assert.equal(messages[0].id, 'aws-mail-1');
assert.equal(messages[0].address, 'tmpjjwk205m0h@edu.email.qlhazycoder.tech');
assert.equal(messages[0].from.emailAddress.address, 'no-reply@signin.aws');
assert.equal(messages[0].subject, '验证您的 AWS 构建者 ID 电子邮件地址');
assert.match(messages[0].bodyPreview, /248680/);
assert.doesNotMatch(messages[0].bodyPreview, /<p/);
});
test('normalized CFTE AWS Builder ID mail matches Kiro verification rules', () => {
const messages = normalizeCloudflareTempEmailMailApiMessages({
data: {
results: [
{
id: 'aws-mail-2',
to: { address: 'tmpjjwk205m0h@edu.email.qlhazycoder.tech' },
from: { address: 'no-reply@signin.aws' },
subject: '验证您的 AWS 构建者 ID 电子邮件地址',
html: '<div>验证码</div><div>248680</div>',
receivedAt: '2026-05-22T09:41:00.000Z',
},
],
},
});
const result = pickVerificationMessageWithTimeFallback(messages, {
afterTimestamp: Date.UTC(2026, 4, 22, 9, 40, 0),
senderFilters: ['no-reply@signin.aws', 'aws'],
subjectFilters: ['aws builder id', 'verification', '验证码', 'code', 'aws'],
requiredKeywords: ['verification', '验证码', 'code', 'aws'],
codePatterns: [
{ source: '(?:verification\\s*code|验证码|Your code is|code is)[:\\s]*(\\d{6})', flags: 'gi' },
{ source: '^\\s*(\\d{6})\\s*$', flags: 'gm' },
{ source: '>\\s*(\\d{6})\\s*<', flags: 'g' },
],
excludeCodes: [],
});
assert.equal(result.match?.message.id, 'aws-mail-2');
assert.equal(result.match?.code, '248680');
});
test('getCloudflareTempEmailAddressFromResponse supports direct and nested response shapes', () => { test('getCloudflareTempEmailAddressFromResponse supports direct and nested response shapes', () => {
assert.equal(getCloudflareTempEmailAddressFromResponse({ address: 'one@example.com' }), 'one@example.com'); assert.equal(getCloudflareTempEmailAddressFromResponse({ address: 'one@example.com' }), 'one@example.com');
assert.equal(getCloudflareTempEmailAddressFromResponse({ data: { address: 'two@example.com' } }), 'two@example.com'); assert.equal(getCloudflareTempEmailAddressFromResponse({ data: { address: 'two@example.com' } }), 'two@example.com');
+33
View File
@@ -194,6 +194,15 @@ test('extractVerificationCode supports runtime mail rule patterns', () => {
); );
}); });
test('extractVerificationCode keeps capture groups when runtime patterns use global flags', () => {
assert.equal(
extractVerificationCode('验证码 248680', {
codePatterns: [{ source: '(?:验证码)[:\\s]*(\\d{6})', flags: 'gi' }],
}),
'248680'
);
});
test('extractVerificationCodeFromMessage reads code from the latest message subject or preview', () => { test('extractVerificationCodeFromMessage reads code from the latest message subject or preview', () => {
assert.equal( assert.equal(
extractVerificationCodeFromMessage({ extractVerificationCodeFromMessage({
@@ -389,6 +398,30 @@ test('pickVerificationMessageWithTimeFallback can ignore afterTimestamp while ke
assert.equal(result.usedTimeFallback, true); assert.equal(result.usedTimeFallback, true);
}); });
test('pickVerificationMessageWithTimeFallback preserves runtime code patterns when only time is relaxed', () => {
const messages = [
{
id: 'old-custom-code-mail',
subject: 'AWS Builder ID verification',
from: { emailAddress: { address: 'no-reply@signin.aws' } },
bodyPreview: 'Use token A661122B to continue.',
receivedDateTime: '2026-05-22T09:41:00.000Z',
},
];
const result = pickVerificationMessageWithTimeFallback(messages, {
afterTimestamp: Date.UTC(2026, 4, 22, 9, 42, 0),
senderFilters: ['signin.aws'],
subjectFilters: ['aws'],
codePatterns: [{ source: 'token\\s+A(\\d{6})B', flags: 'i' }],
excludeCodes: [],
});
assert.equal(result.match.message.id, 'old-custom-code-mail');
assert.equal(result.match.code, '661122');
assert.equal(result.usedTimeFallback, true);
});
test('buildHotmailMailApiLatestUrl includes email, client id, refresh token, and mailbox', () => { test('buildHotmailMailApiLatestUrl includes email, client id, refresh token, and mailbox', () => {
const url = new URL(buildHotmailMailApiLatestUrl({ const url = new URL(buildHotmailMailApiLatestUrl({
apiUrl: 'https://example.com/api/mail-new', apiUrl: 'https://example.com/api/mail-new',