feat(email): expand email client detection & add Outlook edition helper (#819)

* feat(email): significantly expanded email client detection to support 40+ new user agents, including Alpine, Canary Mail, FairEmail, ProtonMail Bridge, Tutanota, and The Bat!
feat(helpers): added getOutlookEdition() utility to interpret raw version strings into specific Outlook editions (e.g., distinguishing Outlook 2016 MSI vs. Click-to-Run/365).

chore(enums): added comprehensive BrowserName.Email enums for all newly supported clients.
chore(types): added TypeScript definitions for the new getOutlookEdition helper.

test(email): added comprehensive test suite covering 60+ email client user agent strings.

test(helpers): added unit tests for getOutlookEdition covering Windows (MSI/C2R) and Mac variants.

* chore: Some small updates for business logic around K-9, Yahoo Mail, Outlook

* test: Edgecase alignment and fixes
chore(deps): npm vulnerability fix in package-lock.json
chore: Updated dist builds

* Revert accidentally-removed additional code and comments

* Correct comment syntax in ua-parser-extensions.js

Fix comment formatting and clean up code.

* chore: build fix
This commit is contained in:
Casey Grimes
2026-01-11 23:34:59 -05:00
committed by GitHub
parent 1696b87b58
commit bab55a28df
11 changed files with 899 additions and 200 deletions

View File

@@ -613,24 +613,66 @@ export const Extension: Readonly<{
},
Email: {
AIRMAIL: 'Airmail',
ALPINE: 'Alpine',
ANDROID_MAIL: 'Android',
APPLE_MAIL: 'Mail',
AQUA_MAIL: 'AquaMail',
BALSA: 'Balsa',
BARCA: 'Barca',
BLUEMAIL: 'BlueMail',
CANARY: 'Canary',
CLAWS_MAIL: 'Claws Mail',
DAUM_MAIL: 'DaumMail',
EVOLUTION: 'Evolution',
EM_CLIENT: 'eM Client',
EUDORA: 'Eudora',
EVOLUTION: 'Evolution',
FAIR_EMAIL: 'FairEmail',
FOXMAIL: 'Foxmail',
GEARY: 'Geary',
GNUS: 'Gnus',
HORDE_IMP: 'Horde::IMP',
IBM_NOTES: 'Lotus-Notes',
INCREDIMAIL: 'IncrediMail',
K9_MAIL: 'K-9 Mail',
KMAIL: 'KMail',
KMAIL2: 'kmail2',
KONTACT: 'Kontact',
MAILBIRD: 'Mailbird',
MAILMATE: 'MailMate',
MAILSPRING: 'Mailspring',
MICROSOFT_OUTLOOK: 'Microsoft Outlook',
MICROSOFT_OUTLOOK_MAC: 'MacOutlook',
MUTT: 'Mutt',
NAVER_MAILAPP: 'NaverMailApp',
NEWTON: 'Newton',
NINE: 'Nine',
NYLAS_MAIL: 'NylasMail',
OUTLOOK_EXPRESS: 'Outlook-Express',
PEGASUS_MAIL: 'Pegasus Mail',
POCOMAIL: 'PocoMail',
POLYMAIL: 'Polymail',
POSTBOX: 'Postbox',
PROTON_MAIL: 'ProtonMail',
PROTON_MAIL_BRIDGE: 'ProtonMail Bridge',
QUALA_MAIL: 'Quala',
R2MAIL2: 'R2Mail2',
RAINLOOP: 'RainLoop',
ROUNDCUBE: 'Roundcube Webmail',
SAMSUNG_EMAIL: 'SamsungEmail',
SPARK_MAIL: 'SparkDesktop',
SPARROW: 'Sparrow',
SPICEBIRD: 'Spicebird',
SQUIRRELMAIL: 'SquirrelMail',
SYLPHEED: 'Sylpheed',
THE_BAT: 'The Bat!',
THUNDERBIRD: 'Thunderbird',
YAHOO_MAIL: 'Yahoo',
TROJITA: 'Trojita',
TURNPIKE: 'Turnpike',
TUTANOTA: 'tutanota-desktop',
WANDERLUST: 'Wanderlust',
WINDOWS_LIVE_MAIL: 'Windows-Live-Mail',
YAHOO_MAIL: 'Yahoo Mail',
YAHOO_MAIL_IOS: 'Yahoo Mail',
ZIMBRA: 'Zimbra',
ZOHO_MAIL: 'ZohoMail-Desktop'
},

View File

@@ -609,24 +609,66 @@ const Extension = Object.freeze({
},
Email: {
AIRMAIL: 'Airmail',
ALPINE: 'Alpine',
ANDROID_MAIL: 'Android',
APPLE_MAIL: 'Mail',
AQUA_MAIL: 'AquaMail',
BALSA: 'Balsa',
BARCA: 'Barca',
BLUEMAIL: 'BlueMail',
CANARY: 'Canary',
CLAWS_MAIL: 'Claws Mail',
DAUM_MAIL: 'DaumMail',
EVOLUTION: 'Evolution',
EM_CLIENT: 'eM Client',
EUDORA: 'Eudora',
EVOLUTION: 'Evolution',
FAIR_EMAIL: 'FairEmail',
FOXMAIL: 'Foxmail',
GEARY: 'Geary',
GNUS: 'Gnus',
HORDE_IMP: 'Horde::IMP',
IBM_NOTES: 'Lotus-Notes',
INCREDIMAIL: 'IncrediMail',
K9_MAIL: 'K-9 Mail',
KMAIL: 'KMail',
KMAIL2: 'kmail2',
KONTACT: 'Kontact',
MAILBIRD: 'Mailbird',
MAILMATE: 'MailMate',
MAILSPRING: 'Mailspring',
MICROSOFT_OUTLOOK: 'Microsoft Outlook',
MICROSOFT_OUTLOOK_MAC: 'MacOutlook',
MUTT: 'Mutt',
NAVER_MAILAPP: 'NaverMailApp',
NEWTON: 'Newton',
NINE: 'Nine',
NYLAS_MAIL: 'NylasMail',
OUTLOOK_EXPRESS: 'Outlook-Express',
PEGASUS_MAIL: 'Pegasus Mail',
POCOMAIL: 'PocoMail',
POLYMAIL: 'Polymail',
POSTBOX: 'Postbox',
PROTON_MAIL: 'ProtonMail',
PROTON_MAIL_BRIDGE: 'ProtonMail Bridge',
QUALA_MAIL: 'Quala',
R2MAIL2: 'R2Mail2',
RAINLOOP: 'RainLoop',
ROUNDCUBE: 'Roundcube Webmail',
SAMSUNG_EMAIL: 'SamsungEmail',
SPARK_MAIL: 'SparkDesktop',
SPARROW: 'Sparrow',
SPICEBIRD: 'Spicebird',
SQUIRRELMAIL: 'SquirrelMail',
SYLPHEED: 'Sylpheed',
THE_BAT: 'The Bat!',
THUNDERBIRD: 'Thunderbird',
YAHOO_MAIL: 'Yahoo',
TROJITA: 'Trojita',
TURNPIKE: 'Turnpike',
TUTANOTA: 'tutanota-desktop',
WANDERLUST: 'Wanderlust',
WINDOWS_LIVE_MAIL: 'Windows-Live-Mail',
YAHOO_MAIL: 'Yahoo Mail',
YAHOO_MAIL_IOS: 'Yahoo Mail',
ZIMBRA: 'Zimbra',
ZOHO_MAIL: 'ZohoMail-Desktop'
},

View File

@@ -613,24 +613,66 @@ const Extension = Object.freeze({
},
Email: {
AIRMAIL: 'Airmail',
ALPINE: 'Alpine',
ANDROID_MAIL: 'Android',
APPLE_MAIL: 'Mail',
AQUA_MAIL: 'AquaMail',
BALSA: 'Balsa',
BARCA: 'Barca',
BLUEMAIL: 'BlueMail',
CANARY: 'Canary',
CLAWS_MAIL: 'Claws Mail',
DAUM_MAIL: 'DaumMail',
EVOLUTION: 'Evolution',
EM_CLIENT: 'eM Client',
EUDORA: 'Eudora',
EVOLUTION: 'Evolution',
FAIR_EMAIL: 'FairEmail',
FOXMAIL: 'Foxmail',
GEARY: 'Geary',
GNUS: 'Gnus',
HORDE_IMP: 'Horde::IMP',
IBM_NOTES: 'Lotus-Notes',
INCREDIMAIL: 'IncrediMail',
K9_MAIL: 'K-9 Mail',
KMAIL: 'KMail',
KMAIL2: 'kmail2',
KONTACT: 'Kontact',
MAILBIRD: 'Mailbird',
MAILMATE: 'MailMate',
MAILSPRING: 'Mailspring',
MICROSOFT_OUTLOOK: 'Microsoft Outlook',
MICROSOFT_OUTLOOK_MAC: 'MacOutlook',
MUTT: 'Mutt',
NAVER_MAILAPP: 'NaverMailApp',
NEWTON: 'Newton',
NINE: 'Nine',
NYLAS_MAIL: 'NylasMail',
OUTLOOK_EXPRESS: 'Outlook-Express',
PEGASUS_MAIL: 'Pegasus Mail',
POCOMAIL: 'PocoMail',
POLYMAIL: 'Polymail',
POSTBOX: 'Postbox',
PROTON_MAIL: 'ProtonMail',
PROTON_MAIL_BRIDGE: 'ProtonMail Bridge',
QUALA_MAIL: 'Quala',
R2MAIL2: 'R2Mail2',
RAINLOOP: 'RainLoop',
ROUNDCUBE: 'Roundcube Webmail',
SAMSUNG_EMAIL: 'SamsungEmail',
SPARK_MAIL: 'SparkDesktop',
SPARROW: 'Sparrow',
SPICEBIRD: 'Spicebird',
SQUIRRELMAIL: 'SquirrelMail',
SYLPHEED: 'Sylpheed',
THE_BAT: 'The Bat!',
THUNDERBIRD: 'Thunderbird',
YAHOO_MAIL: 'Yahoo',
TROJITA: 'Trojita',
TURNPIKE: 'Turnpike',
TUTANOTA: 'tutanota-desktop',
WANDERLUST: 'Wanderlust',
WINDOWS_LIVE_MAIL: 'Windows-Live-Mail',
YAHOO_MAIL: 'Yahoo Mail',
YAHOO_MAIL_IOS: 'Yahoo Mail',
ZIMBRA: 'Zimbra',
ZOHO_MAIL: 'ZohoMail-Desktop'
},

View File

@@ -22,6 +22,19 @@ const INAPP = 'inapp';
const MEDIAPLAYER = 'mediaplayer';
const LIBRARY = 'library';
// Helper to normalize specific email client names
const normalizeEmailName = function (str) {
const map = {
'YahooMobile': 'Yahoo Mail',
'YahooMail': 'Yahoo Mail',
'K-9': 'K-9 Mail',
'K-9 Mail': 'K-9 Mail',
'Zdesktop': 'Zimbra',
'zdesktop': 'Zimbra'
};
return map[str] || str;
};
//////////////////////
// COMMAND LINE APPS
/////////////////////
@@ -229,25 +242,55 @@ const ExtraDevices = Object.freeze({
]
});
///////////////
//////////////
// EMAIL APPS
//////////////
const Emails = Object.freeze({
browser : [
[
// Evolution / Kontact/KMail[2] / [Microsoft/Mac] Outlook / Thunderbird
// Airmail / BlueMail / DaumMail / eMClient / Foxmail / NaverMailApp / Polymail
// ProtonMail / SparkDesktop / Sparrow / Yahoo! Mail / Zimbra / ZohoMail-Desktop
/((?:air|blue|daum|fox|poly|proton)mail|emclient|evolution|kmail2?|kontact|(?:microsoft |mac)?outlook(?:-express)?|navermailapp|(?!chrom.+)sparrow|sparkdesktop|thunderbird|yahoo|zohomail-desktop)(?:m.+ail; |[\/ ])([\w\.]+)/i,
// 1. Specific Android Mail Rule
[/(android)\/([\w\.-]+email)/i],
[NAME, VERSION, [TYPE, EMAIL]],
// Apple's Mail
/(mail)\/([\w\.]+) cf/i
], [NAME, VERSION, [TYPE, EMAIL]], [
// 2. Standard Email Clients
[
new RegExp(
'(' +
// Clients ending in 'mail' (Case 1: Prefix + optional space + [e]mail)
// Covers: AirMail, Claws Mail, FairEmail, SamsungEmail, Yahoo Mail, etc.
'(?:air|aqua|blue|claws|daum|fair|fox|k-9|mac|nylas|pegasus|poco|poly|proton|samsung|squirrel|yahoo) ?e?mail(?:-desktop| app| bridge)?|' +
// Standalone / Specific Names
'microsoft outlook|r2mail2|spicebird|turnpike|yahoomobile|' +
// Microsoft & Outlook Variants
'(?:microsoft )?outlook(?:-express)?|macoutlook|windows-live-mail|' +
// Specific Clients
'alpine|balsa|barca|canary|emclient|eudora|evolution|geary|gnus|' +
'horde::imp|incredimail|kmail2?|kontact|lotus-notes|' +
'mail(?:bird|mate|spring)|mutt|navermailapp|newton|nine|postbox|' +
'rainloop|roundcube webmail|spar(?:row|kdesktop)|sylpheed|' +
'the bat!|thunderbird|trojita|tutanota-desktop|wanderlust|' +
'zdesktop|zohomail-desktop' +
')' +
// Separator
'(?:m.+ail; |[\\/ ])' +
// Version (Updated to allow hyphens for Turnpike)
'([\\w\\.-]+)',
'i'
)
],
[
[NAME, normalizeEmailName],
VERSION,
[TYPE, EMAIL]
],
// 3. Apple Mail Context
[/(mail)\/([\w\.]+) cf/i],
[NAME, VERSION, [TYPE, EMAIL]],
// Zimbra
/zdesktop\/([\w\.]+)/i
], [VERSION, [NAME, 'Zimbra'], [TYPE, EMAIL]]
// 4. Zimbra Server
[/(zimbra)\/([\w\.-]+)/i],
[NAME, VERSION, [TYPE, EMAIL]]
]
});
@@ -451,4 +494,4 @@ module.exports = {
Libraries,
MediaPlayers,
Vehicles
};
};

View File

@@ -26,6 +26,19 @@ const INAPP = 'inapp';
const MEDIAPLAYER = 'mediaplayer';
const LIBRARY = 'library';
// Helper to normalize specific email client names
const normalizeEmailName = function (str) {
const map = {
'YahooMobile': 'Yahoo Mail',
'YahooMail': 'Yahoo Mail',
'K-9': 'K-9 Mail',
'K-9 Mail': 'K-9 Mail',
'Zdesktop': 'Zimbra',
'zdesktop': 'Zimbra'
};
return map[str] || str;
};
//////////////////////
// COMMAND LINE APPS
/////////////////////
@@ -233,25 +246,55 @@ const ExtraDevices = Object.freeze({
]
});
///////////////
//////////////
// EMAIL APPS
//////////////
const Emails = Object.freeze({
browser : [
[
// Evolution / Kontact/KMail[2] / [Microsoft/Mac] Outlook / Thunderbird
// Airmail / BlueMail / DaumMail / eMClient / Foxmail / NaverMailApp / Polymail
// ProtonMail / SparkDesktop / Sparrow / Yahoo! Mail / Zimbra / ZohoMail-Desktop
/((?:air|blue|daum|fox|poly|proton)mail|emclient|evolution|kmail2?|kontact|(?:microsoft |mac)?outlook(?:-express)?|navermailapp|(?!chrom.+)sparrow|sparkdesktop|thunderbird|yahoo|zohomail-desktop)(?:m.+ail; |[\/ ])([\w\.]+)/i,
// 1. Specific Android Mail Rule
[/(android)\/([\w\.-]+email)/i],
[NAME, VERSION, [TYPE, EMAIL]],
// Apple's Mail
/(mail)\/([\w\.]+) cf/i
], [NAME, VERSION, [TYPE, EMAIL]], [
// 2. Standard Email Clients
[
new RegExp(
'(' +
// Clients ending in 'mail' (Case 1: Prefix + optional space + [e]mail)
// Covers: AirMail, Claws Mail, FairEmail, SamsungEmail, Yahoo Mail, etc.
'(?:air|aqua|blue|claws|daum|fair|fox|k-9|mac|nylas|pegasus|poco|poly|proton|samsung|squirrel|yahoo) ?e?mail(?:-desktop| app| bridge)?|' +
// Standalone / Specific Names
'microsoft outlook|r2mail2|spicebird|turnpike|yahoomobile|' +
// Microsoft & Outlook Variants
'(?:microsoft )?outlook(?:-express)?|macoutlook|windows-live-mail|' +
// Specific Clients
'alpine|balsa|barca|canary|emclient|eudora|evolution|geary|gnus|' +
'horde::imp|incredimail|kmail2?|kontact|lotus-notes|' +
'mail(?:bird|mate|spring)|mutt|navermailapp|newton|nine|postbox|' +
'rainloop|roundcube webmail|spar(?:row|kdesktop)|sylpheed|' +
'the bat!|thunderbird|trojita|tutanota-desktop|wanderlust|' +
'zdesktop|zohomail-desktop' +
')' +
// Separator
'(?:m.+ail; |[\\/ ])' +
// Version (Updated to allow hyphens for Turnpike)
'([\\w\\.-]+)',
'i'
)
],
[
[NAME, normalizeEmailName],
VERSION,
[TYPE, EMAIL]
],
// 3. Apple Mail Context
[/(mail)\/([\w\.]+) cf/i],
[NAME, VERSION, [TYPE, EMAIL]],
// Zimbra
/zdesktop\/([\w\.]+)/i
], [VERSION, [NAME, 'Zimbra'], [TYPE, EMAIL]]
// 4. Zimbra Server
[/(zimbra)\/([\w\.-]+)/i],
[NAME, VERSION, [TYPE, EMAIL]]
]
});
@@ -455,4 +498,4 @@ export {
Libraries,
MediaPlayers,
Vehicles
};
};

View File

@@ -55,6 +55,50 @@ const isFromEU = _isFromEU;
*/
const isStandalonePWA = _isStandalonePWA;
/**
* Translates a raw Outlook User-Agent name/version into a
* Developer-friendly Edition (e.g., "Outlook 2019 (Modern Word)").
*/
const getOutlookEdition = (name, version) => {
if (!name || !version) return name;
const cleanName = name.toLowerCase().replace(/microsoft\s+/, '');
// 1. Handle Mac Separately (Different Rendering Engine)
if (cleanName === 'macoutlook') {
const major = parseInt(version.split('.')[0], 10);
if (major >= 16) return "Outlook for Mac (Modern)";
return "Outlook for Mac (Legacy)";
}
// 2. Handle Windows Outlook
if (cleanName === 'outlook') {
const parts = version.split('.').map(Number);
const major = parts[0];
const build = parts[2] || 0; // Build number is usually the 3rd part
// Pre-2016 Versions (Clear Major Version mapping)
if (major === 15) return "Outlook 2013";
if (major === 14) return "Outlook 2010";
if (major === 12) return "Outlook 2007";
if (major < 12) return "Outlook (Legacy)";
// The Version 16.0 Confusion
if (major === 16) {
// Build < 10000 = MSI (Volume License 2016/2019)
// These render poorly (No SVG, older bugs)
if (build < 10000) {
return "Outlook 2016 (MSI / Volume License)";
}
// Build >= 10000 = Click-to-Run (Retail 2016 / 2019 / 365)
// These render well (SVG support, modern CSS)
return "Outlook 365 / 2019+ (Modern)";
}
}
// 3. Fallback for 'Outlook Express' or 'New Outlook' (Browser)
return name;
};
module.exports = {
getDeviceVendor,
isAppleSilicon,
@@ -64,5 +108,6 @@ module.exports = {
isElectron,
isFromEU,
isFrozenUA,
isStandalonePWA
isStandalonePWA,
getOutlookEdition
}

View File

@@ -59,6 +59,50 @@ const isFromEU = _isFromEU;
*/
const isStandalonePWA = _isStandalonePWA;
/**
* Translates a raw Outlook User-Agent name/version into a
* Developer-friendly Edition (e.g., "Outlook 2019 (Modern Word)").
*/
const getOutlookEdition = (name, version) => {
if (!name || !version) return name;
const cleanName = name.toLowerCase().replace(/microsoft\s+/, '');
// 1. Handle Mac Separately (Different Rendering Engine)
if (cleanName === 'macoutlook') {
const major = parseInt(version.split('.')[0], 10);
if (major >= 16) return "Outlook for Mac (Modern)";
return "Outlook for Mac (Legacy)";
}
// 2. Handle Windows Outlook
if (cleanName === 'outlook') {
const parts = version.split('.').map(Number);
const major = parts[0];
const build = parts[2] || 0; // Build number is usually the 3rd part
// Pre-2016 Versions (Clear Major Version mapping)
if (major === 15) return "Outlook 2013";
if (major === 14) return "Outlook 2010";
if (major === 12) return "Outlook 2007";
if (major < 12) return "Outlook (Legacy)";
// The Version 16.0 Confusion
if (major === 16) {
// Build < 10000 = MSI (Volume License 2016/2019)
// These render poorly (No SVG, older bugs)
if (build < 10000) {
return "Outlook 2016 (MSI / Volume License)";
}
// Build >= 10000 = Click-to-Run (Retail 2016 / 2019 / 365)
// These render well (SVG support, modern CSS)
return "Outlook 365 / 2019+ (Modern)";
}
}
// 3. Fallback for 'Outlook Express' or 'New Outlook' (Browser)
return name;
};
export {
getDeviceVendor,
isAppleSilicon,
@@ -68,5 +112,6 @@ export {
isElectron,
isFromEU,
isFrozenUA,
isStandalonePWA
isStandalonePWA,
getOutlookEdition
}