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
+188 -19
View File
@@ -11,6 +11,7 @@
function firstNonEmptyString(values) {
for (const value of values) {
if (value === undefined || value === null) continue;
if (typeof value === 'object') continue;
const normalized = String(value).trim();
if (normalized) return normalized;
}
@@ -87,18 +88,35 @@
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,
const rowKeys = [
'data',
'items',
'messages',
'mails',
'results',
'result',
'rows',
'list',
'hydra:member',
];
const queue = [payload];
const seen = new Set();
for (const candidate of candidates) {
if (Array.isArray(candidate)) {
return candidate;
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)) {
return candidate;
}
if (candidate && typeof candidate === 'object') {
queue.push(candidate);
}
}
}
@@ -106,7 +124,47 @@
}
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 = '') {
@@ -245,19 +303,53 @@
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 = '') {
return String(value || '')
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.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(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/&quot;/gi, '"')
.replace(/&#39;/g, "'")
.replace(/\s+/g, ' ')
.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 = {}) {
const contentType = String(headers['content-type'] || '');
const transferEncoding = String(headers['content-transfer-encoding'] || '').trim().toLowerCase();
@@ -321,40 +413,114 @@
function normalizeCloudflareTempEmailMessage(row = {}) {
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.mail_address,
row.mailAddress,
row.email,
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.originalRecipient,
row.original_recipient_email,
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 parsedMime = raw ? extractTextFromMime(raw) : { headers: {}, text: '' };
const subject = decodeMimeEncodedWords(firstNonEmptyString([
const subject = decodeMimeEncodedWords(firstTextCandidate([
row.subject,
row.title,
parsedMime.headers.subject,
]));
const fromAddress = decodeMimeEncodedWords(firstNonEmptyString([
const fromAddress = decodeMimeEncodedWords(firstTextCandidate([
row.from,
row.sender,
row.mail_from,
row.mailFrom,
row.from_email,
row.fromEmail,
row.sender_email,
row.senderEmail,
parsedMime.headers.from,
], [
'emailAddress',
'address',
'email',
'value',
'name',
'from',
'sender',
]));
const bodyPreview = firstNonEmptyString([
const bodyText = firstBodyTextCandidate([
row.bodyPreview,
row.snippet,
row.text,
row.text_content,
row.textContent,
row.preview,
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,
raw,
]).replace(/\s+/g, ' ').trim();
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,
originalRecipient,
addressId: firstNonEmptyString([row.address_id, row.addressId]),
@@ -369,10 +535,13 @@
receivedDateTime: normalizeReceivedDateTime(firstNonEmptyString([
row.receivedDateTime,
row.received_at,
row.receivedAt,
row.created_at,
row.createdAt,
row.updated_at,
row.updatedAt,
row.date,
row.timestamp,
])),
};
}
+5 -1
View File
@@ -45,7 +45,9 @@
continue;
}
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) {
continue;
}
@@ -252,6 +254,8 @@
excludeCodes: filters.excludeCodes,
senderFilters: filters.senderFilters,
subjectFilters: filters.subjectFilters,
requiredKeywords: filters.requiredKeywords,
codePatterns: filters.codePatterns,
});
return {
+70
View File
@@ -9,6 +9,9 @@ const {
normalizeCloudflareTempEmailDomains,
normalizeCloudflareTempEmailMailApiMessages,
} = require('../cloudflare-temp-email-utils.js');
const {
pickVerificationMessageWithTimeFallback,
} = require('../hotmail-utils.js');
test('normalizeCloudflareTempEmailBaseUrl normalizes host and preserves path', () => {
assert.equal(
@@ -103,6 +106,73 @@ test('normalizeCloudflareTempEmailMailApiMessages decodes multipart quoted print
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', () => {
assert.equal(getCloudflareTempEmailAddressFromResponse({ address: 'one@example.com' }), 'one@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', () => {
assert.equal(
extractVerificationCodeFromMessage({
@@ -389,6 +398,30 @@ test('pickVerificationMessageWithTimeFallback can ignore afterTimestamp while ke
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', () => {
const url = new URL(buildHotmailMailApiLatestUrl({
apiUrl: 'https://example.com/api/mail-new',