Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
/**
* Details of supported OAuth2 Providers.
*/
// When we add a Google mail account, ask for address book and calendar scopes
// as well. Then we can add an address book or calendar without asking again.
//
// Don't ask for all the scopes when adding an address book or calendar
// independently of the mail set-up process. If a mail account already exists,
// we already have a token, and if it doesn't the user is likely to be setting
// up an address book/calendar without wanting mail.
const GOOGLE_SCOPES = {
};
const FASTMAIL_SCOPES = {
};
const MICROSOFT_SCOPES = {
extra: "offline_access",
};
const EWS_SCOPES = {
// "exchange" is used in the account setup, then the config is copied to "ews".
extra: "offline_access",
};
/**
* Map of hostnames to [issuer, scope].
*/
var kHostnames = new Map([
["imap.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]],
["smtp.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]],
["pop.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]],
["imap.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]],
["smtp.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]],
["pop.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]],
["www.googleapis.com", ["accounts.google.com", GOOGLE_SCOPES.carddav]],
[
"apidata.googleusercontent.com",
["accounts.google.com", GOOGLE_SCOPES.caldav],
],
["imap.mail.ru", ["o2.mail.ru", "mail.imap"]],
["smtp.mail.ru", ["o2.mail.ru", "mail.imap"]],
["imap.yandex.com", ["oauth.yandex.com", "mail:imap_full"]],
["smtp.yandex.com", ["oauth.yandex.com", "mail:smtp"]],
["imap.mail.yahoo.com", ["login.yahoo.com", "mail-w"]],
["pop.mail.yahoo.com", ["login.yahoo.com", "mail-w"]],
["smtp.mail.yahoo.com", ["login.yahoo.com", "mail-w"]],
["imap.aol.com", ["login.aol.com", "mail-w"]],
["pop.aol.com", ["login.aol.com", "mail-w"]],
["smtp.aol.com", ["login.aol.com", "mail-w"]],
// outlook.office365.com, smtp.office365.com
["office365.com", ["login.microsoftonline.com", MICROSOFT_SCOPES]],
// autodiscover-s.outlook.com, smtp-mail.outlook.com
["outlook.com", ["login.microsoftonline.com", MICROSOFT_SCOPES]],
// autodiscover.hotmail.com
["hotmail.com", ["login.microsoftonline.com", MICROSOFT_SCOPES]],
["imap.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]],
["pop.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]],
["smtp.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]],
[
"carddav.fastmail.com",
],
["imap.comcast.net", ["comcast.net", COMCAST_SCOPES]],
["pop.comcast.net", ["comcast.net", COMCAST_SCOPES]],
["smtp.comcast.net", ["comcast.net", COMCAST_SCOPES]],
// For testing purposes.
["mochi.test", ["test.test", "test_scope"]],
[
"test.test",
[
"test.test",
{
imap: "test_mail",
pop3: "test_mail",
smtp: "test_mail",
carddav: "test_addressbook",
caldav: "test_calendar",
},
],
],
]);
/**
* Map of issuers to clientId, clientSecret, authorizationEndpoint, tokenEndpoint,
* and usePKCE (RFC7636).
* Issuer is a unique string for the organization that a Thunderbird account
* was registered at.
*
* For the moment these details are hard-coded, since dynamic client
* registration is not yet supported. Don't copy these values for your
* own application - register one for yourself! This code (and possibly even the
* registration itself) will disappear when this is switched to dynamic
* client registration.
*/
var kIssuers = new Map([
[
"accounts.google.com",
{
name: "accounts.google.com",
builtIn: true,
clientId:
"406964657835-aq8lmia8j95dhl1a2bvharmfk3t1hgqj.apps.googleusercontent.com",
clientSecret: "kSmqreRr0qwBWJgbf5Y-PjSU",
},
],
[
"o2.mail.ru",
{
name: "o2.mail.ru",
builtIn: true,
clientId: "thunderbird",
clientSecret: "I0dCAXrcaNFujaaY",
},
],
[
"oauth.yandex.com",
{
name: "oauth.yandex.com",
builtIn: true,
clientId: "2a00bba7374047a6ab79666485ffce31",
clientSecret: "3ded85b4ec574c2187a55dc49d361280",
},
],
[
"login.yahoo.com",
{
name: "login.yahoo.com",
builtIn: true,
clientId:
"dj0yJmk9NUtCTWFMNVpTaVJmJmQ9WVdrOVJ6UjVTa2xJTXpRbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD0yYw--",
clientSecret: "f2de6a30ae123cdbc258c15e0812799010d589cc",
},
],
[
"login.aol.com",
{
name: "login.aol.com",
builtIn: true,
clientId:
"dj0yJmk9OXRHc1FqZHRQYzVvJmQ9WVdrOU1UQnJOR0pvTjJrbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD02NQ--",
clientSecret: "79c1c11991d148ddd02a919000d69879942fc278",
},
],
[
"login.microsoftonline.com",
{
name: "login.microsoftonline.com",
builtIn: true,
clientId: "9e5f94bc-e8a4-4e73-b8be-63364c29d753", // Application (client) ID
authorizationEndpoint:
tokenEndpoint:
},
],
[
"www.fastmail.com",
{
name: "www.fastmail.com",
builtIn: true,
clientId: "35f141ae",
usePKCE: true,
},
],
[
"comcast.net",
{
name: "comcast.net",
builtIn: true,
clientId: "thunderbird-oauth",
clientSecret: "fc5d0a314549bb3d059e0cec751fa4bd40a9cc7b",
usePKCE: true,
},
],
// For testing purposes.
[
"test.test",
{
name: "test.test",
builtIn: true,
clientId: "test_client_id",
clientSecret: "test_secret",
},
],
]);
/**
* OAuth2Providers: Methods to lookup OAuth2 parameters for supported OAuth2
* providers.
*/
export var OAuth2Providers = {
/**
* @typedef hostnameDetails
* @property {string} issuer - A string representing the organization.
* @property {string} allScopes - A space-separated list of all scopes for
* the hostname.
* @property {string} requiredScopes - A space-separated list of all scopes
* required for the given type.
*/
/**
* Map a hostname to the relevant issuer and scope.
*
* @param {string} hostname - The hostname of the server. For example
* "imap.googlemail.com".
* @param {string} type - The type of activity we need a token for,
* e.g. "imap" or "caldav".
* @returns {hostnameDetails} An object containing issuer and scope information
* for the hostname and type, or undefined if not found.
*/
getHostnameDetails(hostname, type) {
if (!type) {
throw new Error("passing a `type` argument is required");
}
if (type.startsWith("owl")) {
type = "exchange";
}
const details = this._getHostnameDetails(hostname);
if (!details) {
// No data, return.
return undefined;
}
let [issuer, scopes] = details;
if (
issuer == "login.microsoftonline.com" &&
["ews", "exchange"].includes(type)
) {
// Special case for EWS, to avoid asking for the scope when not needed.
scopes = EWS_SCOPES;
}
if (typeof scopes == "string") {
// Scopes not separated into types.
return { issuer, allScopes: scopes, requiredScopes: scopes };
}
const allScopes = combineScopes(Object.values(scopes));
if (!scopes[type]) {
// No data for type.
return undefined;
}
const requiredScopes = combineScopes([scopes[type], scopes.extra]);
return { issuer, allScopes, requiredScopes };
},
_getHostnameDetails(hostname) {
// During CardDAV SRV autodiscovery, rfc6764#section-6 says:
//
// * The client will need to make authenticated HTTP requests to
// the service. Typically, a "user identifier" is required for
// some form of user/password authentication. When a user
// identifier is required, clients MUST first use the "mailbox"
//
// However macOS Contacts does not do this and just uses the "localpart"
// instead. To work around this bug, during SRV autodiscovery Fastmail
// returns SRV records of the form '0 1 443 d[0-9]+.carddav.fastmail.com.'
// which encodes the internal domainid of the queried SRV domain in the
// sub-domain of the Target (rfc2782) of the SRV result. This can
// then be extracted from the Host header on each DAV request, the
// original domain looked up and attached to the "localpart" to create
// a full "mailbox", allowing autodiscovery to just work for usernames
// in any domain including self hosted domains.
//
// So for this hostname -> issuer/scope lookup to work, we need to
// look not just at the hostname, but also any sub-domains of this
// hostname.
while (hostname.includes(".")) {
const foundHost = kHostnames.get(hostname);
if (foundHost) {
return foundHost;
}
hostname = hostname.replace(/^[^.]*[.]/, "");
}
return undefined;
},
/**
* Map an issuer to OAuth2 account details.
*
* @param {string} issuer - The organization issuing OAuth2 parameters, e.g.
* "accounts.google.com".
*
* @returns {Array} An array containing [clientId, clientSecret, authorizationEndpoint, tokenEndpoint].
* clientId and clientSecret are strings representing the account registered
* for Thunderbird with the organization.
* authorizationEndpoint and tokenEndpoint are url strings representing
* endpoints to access OAuth2 authentication.
*/
getIssuerDetails(issuer) {
return kIssuers.get(issuer);
},
/**
* Add a provider at run-time. This will typically only be called by the
* extension API.
*
* @param {string} issuer - To identify this provider in the login manager.
* @param {string} clientId - Identifies the OAuth client to the server.
* @param {string} clientSecret - Identifies the OAuth client to the server.
* @param {string} authorizationEndpoint - OAuth authorization endpoint address.
* @param {string} tokenEndpoint - OAuth token endpoint address.
* @param {string} redirectionEndpoint - OAuth redirection endpoint.
* @param {boolean} usePKCE - If the authorization uses PKCE.
* @param {string[]} hostnames - One or more hostnames which use this OAuth provider.
* @param {string} scopes - The scopes to request when using this OAuth provider.
*/
registerProvider(
issuer,
clientId,
clientSecret,
authorizationEndpoint,
tokenEndpoint,
redirectionEndpoint,
usePKCE,
hostnames,
scopes
) {
if (kIssuers.has(issuer)) {
throw new Error(`Issuer ${issuer} already registered.`);
}
for (const hostname of hostnames) {
if (kHostnames.has(hostname)) {
throw new Error(`Hostname ${hostname} already registered.`);
}
}
kIssuers.set(issuer, {
name: issuer,
builtIn: false,
clientId,
clientSecret,
authorizationEndpoint,
tokenEndpoint,
redirectionEndpoint,
usePKCE,
});
for (const hostname of hostnames) {
kHostnames.set(hostname, [issuer, scopes]);
}
},
/**
* Remove a runtime-added provider. Built-in providers cannot be removed.
*
* @param {string} issuer - The same string used for `registerProvider`.
*/
unregisterProvider(issuer) {
if (!kIssuers.has(issuer)) {
throw new Error(`Issuer ${issuer} was not registered.`);
}
if (kIssuers.get(issuer).builtIn) {
throw new Error(`Refusing to unregister built-in provider ${issuer}.`);
}
kIssuers.delete(issuer);
for (const [hostname, details] of kHostnames) {
if (details[0] == issuer) {
kHostnames.delete(hostname);
}
}
},
};
/**
* Turns zero or more space-delimited strings of scopes into a single string,
* avoiding duplicates.
*
* @param {string[]} scopeStrings
* @returns {string}
*/
function combineScopes(scopeStrings) {
const scopes = new Set();
for (const scopeString of scopeStrings) {
if (!scopeString) {
continue;
}
for (const scope of scopeString.split(" ")) {
scopes.add(scope);
}
}
return [...scopes].join(" ");
}