src / accounts.ts
src / accounts.ts
import type { ImapConfig } from "./imap";
import type { SmtpConfig } from "./smtp";
export interface AccountInfo {
slot: 1 | 2 | 3;
label: string;
imap: ImapConfig;
smtp: SmtpConfig | null;
}
interface ConfigReader {
get(key: string): unknown;
}
function readString(cfg: ConfigReader, key: string): string {
const value = cfg.get(key);
return typeof value === "string" ? value.trim() : "";
}
function readNumber(cfg: ConfigReader, key: string, fallback: number): number {
const value = Number(cfg.get(key));
return Number.isInteger(value) && value >= 1 && value <= 65535 ? value : fallback;
}
function readSlot(cfg: ConfigReader, n: 1 | 2 | 3): AccountInfo | null {
const label = readString(cfg, `account${n}Label`);
const host = readString(cfg, `account${n}ImapHost`);
const user = readString(cfg, `account${n}User`);
const passwordValue = cfg.get(`account${n}Password`);
const password = typeof passwordValue === "string" ? passwordValue : "";
if (!host || !user || !password) return null;
const smtpHost = readString(cfg, `account${n}SmtpHost`);
return {
slot: n,
label: label || `Account ${n}`,
imap: {
host,
port: readNumber(cfg, `account${n}ImapPort`, 993),
user,
password,
},
smtp: smtpHost
? {
host: smtpHost,
port: readNumber(cfg, `account${n}SmtpPort`, 587),
user,
password,
}
: null,
};
}
export function getConfiguredAccounts(cfg: ConfigReader): AccountInfo[] {
return ([1, 2, 3] as const)
.map((n) => readSlot(cfg, n))
.filter((account): account is AccountInfo => account !== null);
}
export function resolveAccount(cfg: ConfigReader, accountRef: string): AccountInfo {
const accounts = getConfiguredAccounts(cfg);
if (accounts.length === 0) {
throw new Error(
"No email accounts configured. Add IMAP Host, Email Address, and Password for at least one account in plugin settings.",
);
}
const ref = accountRef.trim();
if (!ref || ref.toLowerCase() === "default") return accounts[0];
// Accept only an exact slot number. Strings such as "2foo" must not resolve to slot 2.
if (/^[1-3]$/.test(ref)) {
const slotNumber = Number(ref);
const found = accounts.find((account) => account.slot === slotNumber);
if (!found) throw new Error(`Account ${slotNumber} is not configured. Check plugin settings.`);
return found;
}
const labelMatches = accounts.filter(
(account) => account.label.toLowerCase() === ref.toLowerCase(),
);
if (labelMatches.length === 1) return labelMatches[0];
if (labelMatches.length > 1) {
const slots = labelMatches.map((account) => account.slot).join(", ");
throw new Error(
`Account label "${ref}" is ambiguous because it is used by slots ${slots}. Use a slot number or assign unique labels.`,
);
}
const list = accounts.map((account) => `${account.slot} (${account.label})`).join(", ");
throw new Error(`Account "${ref}" not found. Configured accounts: ${list}`);
}
import type { ImapConfig } from "./imap";
import type { SmtpConfig } from "./smtp";
export interface AccountInfo {
slot: 1 | 2 | 3;
label: string;
imap: ImapConfig;
smtp: SmtpConfig | null;
}
interface ConfigReader {
get(key: string): unknown;
}
function readString(cfg: ConfigReader, key: string): string {
const value = cfg.get(key);
return typeof value === "string" ? value.trim() : "";
}
function readNumber(cfg: ConfigReader, key: string, fallback: number): number {
const value = Number(cfg.get(key));
return Number.isInteger(value) && value >= 1 && value <= 65535 ? value : fallback;
}
function readSlot(cfg: ConfigReader, n: 1 | 2 | 3): AccountInfo | null {
const label = readString(cfg, `account${n}Label`);
const host = readString(cfg, `account${n}ImapHost`);
const user = readString(cfg, `account${n}User`);
const passwordValue = cfg.get(`account${n}Password`);
const password = typeof passwordValue === "string" ? passwordValue : "";
if (!host || !user || !password) return null;
const smtpHost = readString(cfg, `account${n}SmtpHost`);
return {
slot: n,
label: label || `Account ${n}`,
imap: {
host,
port: readNumber(cfg, `account${n}ImapPort`, 993),
user,
password,
},
smtp: smtpHost
? {
host: smtpHost,
port: readNumber(cfg, `account${n}SmtpPort`, 587),
user,
password,
}
: null,
};
}
export function getConfiguredAccounts(cfg: ConfigReader): AccountInfo[] {
return ([1, 2, 3] as const)
.map((n) => readSlot(cfg, n))
.filter((account): account is AccountInfo => account !== null);
}
export function resolveAccount(cfg: ConfigReader, accountRef: string): AccountInfo {
const accounts = getConfiguredAccounts(cfg);
if (accounts.length === 0) {
throw new Error(
"No email accounts configured. Add IMAP Host, Email Address, and Password for at least one account in plugin settings.",
);
}
const ref = accountRef.trim();
if (!ref || ref.toLowerCase() === "default") return accounts[0];
// Accept only an exact slot number. Strings such as "2foo" must not resolve to slot 2.
if (/^[1-3]$/.test(ref)) {
const slotNumber = Number(ref);
const found = accounts.find((account) => account.slot === slotNumber);
if (!found) throw new Error(`Account ${slotNumber} is not configured. Check plugin settings.`);
return found;
}
const labelMatches = accounts.filter(
(account) => account.label.toLowerCase() === ref.toLowerCase(),
);
if (labelMatches.length === 1) return labelMatches[0];
if (labelMatches.length > 1) {
const slots = labelMatches.map((account) => account.slot).join(", ");
throw new Error(
`Account label "${ref}" is ambiguous because it is used by slots ${slots}. Use a slot number or assign unique labels.`,
);
}
const list = accounts.map((account) => `${account.slot} (${account.label})`).join(", ");
throw new Error(`Account "${ref}" not found. Configured accounts: ${list}`);
}