fix cloudflare temp email aws code parsing
This commit is contained in:
+188
-19
@@ -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(/ /gi, ' ')
|
||||
.replace(/&/gi, '&')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/"/gi, '"')
|
||||
.replace(/'/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
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user