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