Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions background.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ importScripts(
'flows/kiro/background/desktop-authorize-runner.js',
'flows/kiro/background/publisher-kiro-rs.js',
'flows/grok/background/publisher-webchat2api.js',
'background/email-local-part-helpers.js',
'background/generated-email-helpers.js',
'background/signup-flow-helpers.js',
'background/mail-rule-registry.js',
Expand Down
13 changes: 11 additions & 2 deletions background/cloudmail-provider.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(function cloudMailProviderModule(root, factory) {
root.MultiPageBackgroundCloudMailProvider = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createCloudMailProviderModule() {
root.MultiPageBackgroundCloudMailProvider = factory(root);
})(typeof self !== 'undefined' ? self : globalThis, function createCloudMailProviderModule(root = globalThis) {
function createCloudMailProvider(deps = {}) {
const {
addLog = async () => {},
Expand All @@ -18,6 +18,7 @@
normalizeCloudMailDomains,
normalizeCloudMailMailApiMessages,
persistRegistrationEmailState = null,
buildRandomNameDateTimeLocalPart = root.MultiPageEmailLocalPartHelpers?.buildRandomNameDateTimeLocalPart,
pickVerificationMessageWithTimeFallback,
setEmailState = async () => {},
setPersistentSettings = async () => {},
Expand Down Expand Up @@ -172,6 +173,12 @@
return chars.join('');
}

function buildCloudMailNameDateTimeLocalPart(date = new Date()) {
return typeof buildRandomNameDateTimeLocalPart === 'function'
? buildRandomNameDateTimeLocalPart(date)
: '';
}

async function fetchCloudMailAddress(state, options = {}) {
throwIfStopped();
const latestState = state || await getState();
Expand All @@ -181,6 +188,7 @@
requireDomain: true,
});
const requestedLocal = String(options.localPart || options.name || '').trim().toLowerCase()
|| buildCloudMailNameDateTimeLocalPart(options.date)
|| generateCloudMailAliasLocalPart();
const address = `${requestedLocal}@${ensuredConfig.domain}`.toLowerCase();
const payload = { list: [{ email: address }] };
Expand Down Expand Up @@ -318,6 +326,7 @@
}

return {
buildCloudMailNameDateTimeLocalPart,
ensureCloudMailConfig,
ensureCloudMailToken,
fetchCloudMailAddress,
Expand Down
78 changes: 78 additions & 0 deletions background/email-local-part-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
(function attachEmailLocalPartHelpers(root, factory) {
root.MultiPageEmailLocalPartHelpers = factory(root);
})(typeof self !== 'undefined' ? self : globalThis, function createEmailLocalPartHelpersModule(root = globalThis) {
const ENGLISH_NAME_PREFIXES = [
'james', 'john', 'robert', 'michael', 'william', 'david', 'richard', 'joseph',
'thomas', 'charles', 'mary', 'patricia', 'jennifer', 'linda', 'elizabeth',
'barbara', 'susan', 'jessica', 'sarah', 'karen', 'daniel', 'matthew',
'anthony', 'mark', 'donald', 'steven', 'paul', 'andrew', 'joshua', 'kevin',
'brian', 'george', 'edward', 'ronald', 'timothy', 'jason', 'jeffrey', 'ryan',
'jacob', 'gary', 'nicholas', 'eric', 'jonathan', 'stephen', 'larry', 'justin',
'scott', 'brandon', 'benjamin', 'samuel', 'gregory', 'alexander', 'patrick',
'frank', 'raymond', 'jack', 'dennis', 'jerry', 'tyler', 'aaron', 'henry',
'douglas', 'peter', 'adam', 'zachary', 'nathan', 'walter', 'harold', 'kyle',
'carl', 'arthur', 'gerald', 'roger', 'alice', 'emma', 'olivia', 'sophia',
'isabella', 'mia', 'amelia', 'harper', 'evelyn', 'abigail', 'emily', 'ella',
'scarlett', 'grace', 'chloe', 'victoria', 'riley', 'aria', 'lily', 'nora',
];

const RANDOM_SUFFIX_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789';

function getRandomInt(maxExclusive) {
const max = Math.floor(Number(maxExclusive));
if (!Number.isFinite(max) || max <= 0) return 0;
const cryptoApi = root.crypto;
if (cryptoApi && typeof cryptoApi.getRandomValues === 'function') {
const buffer = new Uint32Array(1);
cryptoApi.getRandomValues(buffer);
return buffer[0] % max;
}
return Math.floor(Math.random() * max);
}

function pickRandomEnglishNamePrefix() {
return ENGLISH_NAME_PREFIXES[getRandomInt(ENGLISH_NAME_PREFIXES.length)] || 'james';
}

function formatDateTimeDigits(date = new Date()) {
const current = new Date(date);
if (Number.isNaN(current.getTime())) {
return '';
}
const year = String(current.getFullYear());
const month = String(current.getMonth() + 1).padStart(2, '0');
const day = String(current.getDate()).padStart(2, '0');
const hour = String(current.getHours()).padStart(2, '0');
const minute = String(current.getMinutes()).padStart(2, '0');
const second = String(current.getSeconds()).padStart(2, '0');
const millisecond = String(current.getMilliseconds()).padStart(3, '0');
return `${year}${month}${day}${hour}${minute}${second}${millisecond}`;
}

function buildRandomAlphaNumericSuffix(length = 4) {
const size = Math.max(0, Math.floor(Number(length) || 0));
const chars = [];
for (let index = 0; index < size; index += 1) {
chars.push(RANDOM_SUFFIX_CHARS[getRandomInt(RANDOM_SUFFIX_CHARS.length)] || 'a');
}
return chars.join('');
}

function buildRandomNameDateTimeLocalPart(date = new Date(), options = {}) {
const dateTimeDigits = formatDateTimeDigits(date);
if (!dateTimeDigits) {
return '';
}
const suffixLength = Number.isFinite(Number(options.suffixLength))
? Math.max(0, Math.floor(Number(options.suffixLength)))
: 4;
return `${pickRandomEnglishNamePrefix()}${dateTimeDigits}${buildRandomAlphaNumericSuffix(suffixLength)}`;
}

return {
buildRandomAlphaNumericSuffix,
buildRandomNameDateTimeLocalPart,
formatDateTimeDigits,
pickRandomEnglishNamePrefix,
};
});
17 changes: 14 additions & 3 deletions background/generated-email-helpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(function attachGeneratedEmailHelpers(root, factory) {
root.MultiPageGeneratedEmailHelpers = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createGeneratedEmailHelpersModule() {
root.MultiPageGeneratedEmailHelpers = factory(root);
})(typeof self !== 'undefined' ? self : globalThis, function createGeneratedEmailHelpersModule(root = globalThis) {
function createGeneratedEmailHelpers(deps = {}) {
const {
addLog,
Expand All @@ -24,6 +24,7 @@
normalizeEmailGenerator,
isGeneratedAliasProvider,
persistRegistrationEmailState = null,
buildRandomNameDateTimeLocalPart = root.MultiPageEmailLocalPartHelpers?.buildRandomNameDateTimeLocalPart,
reuseOrCreateTab,
sendToContentScript,
setEmailState,
Expand Down Expand Up @@ -59,6 +60,12 @@
return chars.join('');
}

function buildDefaultGeneratedEmailLocalPart(date = new Date()) {
return typeof buildRandomNameDateTimeLocalPart === 'function'
? buildRandomNameDateTimeLocalPart(date)
: '';
}

async function fetchCloudflareEmail(state, options = {}) {
throwIfStopped();
const latestState = state || await getState();
Expand Down Expand Up @@ -165,7 +172,9 @@
requireAdminAuth: true,
requireDomain: true,
});
const requestedName = String(options.localPart || options.name || '').trim().toLowerCase() || generateCloudflareAliasLocalPart();
const requestedName = String(options.localPart || options.name || '').trim().toLowerCase()
|| buildDefaultGeneratedEmailLocalPart(options.date)
|| generateCloudflareAliasLocalPart();
const effectiveDomain = config.effectiveDomain || (
typeof buildCloudflareTempEmailEffectiveDomain === 'function'
? buildCloudflareTempEmailEffectiveDomain(config)
Expand Down Expand Up @@ -377,6 +386,8 @@
fetchCloudflareTempEmailAddress,
fetchDuckEmail,
fetchGeneratedEmail,
buildDefaultGeneratedEmailLocalPart,
buildRandomNameDateTimeLocalPart,
generateCloudflareAliasLocalPart,
requestCloudflareTempEmailJson,
};
Expand Down
106 changes: 104 additions & 2 deletions tests/background-generated-email-module.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,38 @@ const assert = require('node:assert/strict');
const fs = require('node:fs');

function loadGeneratedEmailHelpersApi() {
const localPartHelpersSource = fs.readFileSync('background/email-local-part-helpers.js', 'utf8');
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
const globalScope = {};
return new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
return new Function('self', `${localPartHelpersSource}; ${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
}

test('background imports generated email helper module', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(source, /importScripts\([\s\S]*'background\/email-local-part-helpers\.js'/);
assert.match(source, /importScripts\([\s\S]*'background\/generated-email-helpers\.js'/);
});

test('email local-part helper builds english name, date-time, and random suffix', () => {
const source = fs.readFileSync('background/email-local-part-helpers.js', 'utf8');
let randomValues = [];
const globalScope = {
crypto: {
getRandomValues: (buffer) => {
buffer[0] = randomValues.shift() || 0;
return buffer;
},
},
};
const api = new Function('self', `${source}; return self.MultiPageEmailLocalPartHelpers;`)(globalScope);

assert.equal(api.formatDateTimeDigits('2026-05-17T08:09:10.123'), '20260517080910123');
randomValues = [0, 1, 2, 3];
assert.equal(api.buildRandomAlphaNumericSuffix(4), 'abcd');
randomValues = [0, 0, 1, 2, 3];
assert.equal(api.buildRandomNameDateTimeLocalPart('2026-05-17T08:09:10.123'), 'james20260517080910123abcd');
});

test('generated email helper module exposes a factory', () => {
const api = loadGeneratedEmailHelpersApi();

Expand Down Expand Up @@ -481,7 +503,7 @@ test('generated email helper uses the regular temp email domain when random subd
name: requests[0].body.name,
domain: 'mail.example.com',
});
assert.match(requests[0].body.name, /^[a-z0-9]+$/);
assert.match(requests[0].body.name, /^[a-z]+[0-9]{17}[a-z0-9]{4}$/);
});

test('generated email helper requests random subdomain creation while preserving the returned address', async () => {
Expand Down Expand Up @@ -639,6 +661,86 @@ test('generated email helper uses fixed subdomain as the effective temp email do
});
});

test('generated email helper combines default name format with fixed subdomain payload', async () => {
const api = loadGeneratedEmailHelpersApi();
const requests = [];
const savedEmails = [];

const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build managed alias');
},
buildCloudflareTempEmailEffectiveDomain: (config) => `${config.subdomainPrefix}.${config.domain}`,
buildCloudflareTempEmailHeaders: () => ({ 'x-admin-auth': 'admin-secret' }),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async (url, options = {}) => {
requests.push({
url,
method: options.method,
body: options.body ? JSON.parse(options.body) : null,
});
return {
ok: true,
text: async () => JSON.stringify({ address: `${requests[0].body.name}@team.mail.example.com` }),
};
},
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: (payload) => payload.address,
getCloudflareTempEmailConfig: () => ({
baseUrl: 'https://temp.example.com',
adminAuth: 'admin-secret',
customAuth: '',
effectiveDomain: 'team.mail.example.com',
useRandomSubdomain: true,
useFixedSubdomain: true,
subdomainPrefix: 'team',
domain: 'mail.example.com',
}),
getState: async () => ({
mailProvider: '163',
emailGenerator: 'cloudflare-temp-email',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate mail2925 account');
},
joinCloudflareTempEmailUrl: (baseUrl, path) => `${baseUrl}${path}`,
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: (value) => String(value || '').trim().toLowerCase(),
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: () => false,
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not use duck generator');
},
setEmailState: async (email) => {
savedEmails.push(email);
},
throwIfStopped: () => {},
});

const email = await helpers.fetchGeneratedEmail({
emailGenerator: 'cloudflare-temp-email',
}, {
generator: 'cloudflare-temp-email',
date: '2026-05-17T08:09:10.123',
});

assert.match(email, /^[a-z]+20260517080910123[a-z0-9]{4}@team\.mail\.example\.com$/);
assert.deepEqual(savedEmails, [email]);
assert.equal(requests.length, 1);
assert.deepEqual(requests[0].body, {
enablePrefix: true,
enableRandomSubdomain: false,
name: requests[0].body.name,
domain: 'team.mail.example.com',
});
assert.match(requests[0].body.name, /^[a-z]+20260517080910123[a-z0-9]{4}$/);
});

test('generated email helper honors iCloud always-new fetch mode', async () => {
const api = loadGeneratedEmailHelpersApi();
const icloudOptions = [];
Expand Down
54 changes: 54 additions & 0 deletions tests/cloudmail-provider.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const test = require('node:test');
const assert = require('node:assert/strict');

require('../background/email-local-part-helpers.js');
require('../background/cloudmail-provider.js');

function createProviderApi(options = {}) {
Expand Down Expand Up @@ -228,3 +229,56 @@ test('fetchCloudMailAddress preserves phone identity through the shared persiste
},
]);
});

test('fetchCloudMailAddress builds english name, date-time, and suffix local part by default', async () => {
const api = globalThis.MultiPageBackgroundCloudMailProvider.createCloudMailProvider({
addLog: async () => {},
buildCloudMailHeaders: () => ({}),
CLOUD_MAIL_DEFAULT_PAGE_SIZE: 20,
CLOUD_MAIL_GENERATOR: 'cloudmail',
CLOUD_MAIL_PROVIDER: 'cloudmail',
fetchImpl: async (url) => {
if (String(url).includes('/api/public/addUser')) {
return {
ok: true,
text: async () => JSON.stringify({ code: 200 }),
};
}
return {
ok: true,
text: async () => JSON.stringify({ code: 200, data: { token: 'token' } }),
};
},
getCloudMailTokenFromResponse: () => 'token',
getState: async () => ({}),
joinCloudMailUrl: (baseUrl, path) => `${baseUrl}${path}`,
normalizeCloudMailAddress: (value) => String(value || '').trim().toLowerCase(),
normalizeCloudMailBaseUrl: (value) => String(value || '').trim(),
normalizeCloudMailDomain: (value) => String(value || '').trim(),
normalizeCloudMailDomains: (values) => values || [],
normalizeCloudMailMailApiMessages: () => [],
persistRegistrationEmailState: async () => {},
pickVerificationMessageWithTimeFallback: () => ({
match: null,
usedRelaxedFilters: false,
usedTimeFallback: false,
}),
setEmailState: async () => {},
setPersistentSettings: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});

const state = {
cloudMailBaseUrl: 'https://mail.example.com',
cloudMailAdminEmail: 'admin@example.com',
cloudMailAdminPassword: 'secret',
cloudMailToken: 'token',
cloudMailDomain: 'example.com',
};
const email = await api.fetchCloudMailAddress(state, {
date: '2026-05-17T08:09:10.123',
});

assert.match(email, /^[a-z]+20260517080910123[a-z0-9]{4}@example\.com$/);
});