mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 08:16:22 +00:00
118 lines
4.2 KiB
TypeScript
118 lines
4.2 KiB
TypeScript
/**
|
|
* Server settings for other mail apps (Thunderbird, Apple Mail, Outlook,
|
|
* phone apps), read from the mail server's Mozilla autoconfig document.
|
|
*
|
|
* Stalwart 0.16 and 1.0 both serve
|
|
* `/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=<address>`
|
|
* without sign-in and with `Access-Control-Allow-Origin: *`, so the page reads
|
|
* it directly from the server it is signed in to. No other host is asked.
|
|
*/
|
|
|
|
import { desktopJmapBase } from '@/lib/desktop/jmap-route';
|
|
|
|
export type MailSocketSecurity = 'ssl' | 'starttls' | 'none';
|
|
|
|
export interface MailServerEndpoint {
|
|
hostname: string;
|
|
port: number;
|
|
security: MailSocketSecurity;
|
|
}
|
|
|
|
export interface MailClientConfig {
|
|
username?: string;
|
|
imap?: MailServerEndpoint;
|
|
smtp?: MailServerEndpoint;
|
|
caldav?: string;
|
|
carddav?: string;
|
|
}
|
|
|
|
function text(el: Element | null | undefined, tag: string): string | undefined {
|
|
const value = el?.getElementsByTagName(tag)[0]?.textContent?.trim();
|
|
return value ? value : undefined;
|
|
}
|
|
|
|
function security(raw: string | undefined): MailSocketSecurity {
|
|
const value = (raw ?? '').toUpperCase();
|
|
if (value === 'SSL' || value === 'TLS') return 'ssl';
|
|
if (value === 'STARTTLS') return 'starttls';
|
|
return 'none';
|
|
}
|
|
|
|
function endpoint(el: Element | undefined): MailServerEndpoint | undefined {
|
|
if (!el) return undefined;
|
|
const hostname = text(el, 'hostname');
|
|
const port = Number(text(el, 'port'));
|
|
if (!hostname || !Number.isInteger(port) || port <= 0 || port > 65535) return undefined;
|
|
return { hostname, port, security: security(text(el, 'socketType')) };
|
|
}
|
|
|
|
function davUrl(raw: string | undefined): string | undefined {
|
|
if (!raw) return undefined;
|
|
try {
|
|
const url = new URL(raw);
|
|
return url.protocol === 'https:' || url.protocol === 'http:' ? url.toString() : undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The parts of an autoconfig document a person needs to set up another app.
|
|
* Returns null for anything that isn't a clientConfig with at least one server.
|
|
*/
|
|
export function parseMailClientConfig(xml: string): MailClientConfig | null {
|
|
let doc: Document;
|
|
try {
|
|
doc = new DOMParser().parseFromString(xml, 'application/xml');
|
|
} catch {
|
|
return null;
|
|
}
|
|
if (doc.getElementsByTagName('parsererror').length > 0) return null;
|
|
const root = doc.documentElement;
|
|
if (!root || root.nodeName !== 'clientConfig') return null;
|
|
|
|
const incoming = Array.from(root.getElementsByTagName('incomingServer'));
|
|
const outgoing = Array.from(root.getElementsByTagName('outgoingServer'));
|
|
const imapEl = incoming.find((el) => el.getAttribute('type') === 'imap');
|
|
const smtpEl = outgoing.find((el) => el.getAttribute('type') === 'smtp');
|
|
const calendarEl = Array.from(root.getElementsByTagName('calendar')).find((el) => el.getAttribute('type') === 'caldav');
|
|
const contactsEl = Array.from(root.getElementsByTagName('addressBook')).find((el) => el.getAttribute('type') === 'carddav');
|
|
|
|
const config: MailClientConfig = {
|
|
username: text(imapEl, 'username') ?? text(smtpEl, 'username'),
|
|
imap: endpoint(imapEl),
|
|
smtp: endpoint(smtpEl),
|
|
caldav: davUrl(text(calendarEl, 'serverURL')),
|
|
carddav: davUrl(text(contactsEl, 'serverURL')),
|
|
};
|
|
if (!config.imap && !config.smtp && !config.caldav && !config.carddav) return null;
|
|
return config;
|
|
}
|
|
|
|
/** The autoconfig address on the server the page is signed in to. */
|
|
export function mailClientConfigUrl(serverUrl: string, emailAddress: string): string {
|
|
const base = desktopJmapBase(serverUrl.replace(/\/+$/, ''));
|
|
return `${base}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=${encodeURIComponent(emailAddress)}`;
|
|
}
|
|
|
|
/** Reads and parses the server's autoconfig; null when it has none or can't be reached. */
|
|
export async function fetchMailClientConfig(
|
|
serverUrl: string,
|
|
emailAddress: string,
|
|
fetchImpl: typeof fetch = fetch,
|
|
signal?: AbortSignal,
|
|
): Promise<MailClientConfig | null> {
|
|
if (!serverUrl || !emailAddress.includes('@')) return null;
|
|
try {
|
|
const response = await fetchImpl(mailClientConfigUrl(serverUrl, emailAddress), {
|
|
credentials: 'omit',
|
|
cache: 'no-store',
|
|
signal,
|
|
});
|
|
if (!response.ok) return null;
|
|
return parseMailClientConfig(await response.text());
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|