/* * Contains all of the logic for interacting with JMAP * None of the dependencies should leak, in types or otherwise */ import * as base64 from "base-64"; import * as jmapclient from "jmap-client-ts"; import { FetchTransport } from "jmap-client-ts/lib/utils/fetch-transport"; import { IAccount, IEmail, IMailbox, ISession } from "./types"; type Callback = () => void; export interface IAuth { email: string; password: string; } export interface ClientState { session: ISession | null; } export default class Client { // Get the currently active account account(accountId: string): IAccount | null { if (!(this.state.session && this.state.session.accounts)) return null; return this.state.session.accounts[accountId]; } // All objects which currently are listening for changes callbacks: Array = []; jclient: jmapclient.Client | null = null; state: ClientState = { session: null, }; onChange(f: Callback) { this.callbacks.push(f); } // Ensure we have the fully-fleshed email ensureEmailGet(accountId: string, emailId: string) { if (this.state.session == null) return; const existing = this.state.session.emails[emailId]; if (existing != null) return; this.emailGet(accountId, emailId); } // Ensure we have the list of emails for the provided account and mailbox ensureEmailList(accountId: string, mailboxId: string) { const mailbox = this.mailbox(accountId, mailboxId); if (mailbox != null && mailbox.emailIds != null) return; this.emailList(accountId, mailboxId, []); } // Ensure that login happens. If this is called many times, only login once. ensureLogin(auth: IAuth) { if (this.jclient != null) return; this.doLogin(auth); } // Make the request to get system metadata doLogin(auth: IAuth) { const domain = auth.email.split("@")[1]; const well_known_url = "https://" + domain + "/.well-known/jmap"; const basic_auth = "Basic " + base64.encode(auth.email + ":" + auth.password); this.jclient = new jmapclient.Client({ accessToken: "fake token", httpHeaders: { Authorization: basic_auth }, sessionUrl: well_known_url, transport: new FetchTransport(fetch.bind(window)), }); this.jclient .fetchSession() .then(() => { this._onSession(); }) .catch((error) => console.error(error)); return; } email(emailId: string): IEmail | null { if (this.state.session == null) return null; return this.state.session.emails[emailId]; } emailGet(accountId: string, emailId: string) { if (this.jclient == null) return; this.jclient .email_get({ accountId: accountId, ids: [emailId], properties: ["mailboxIds", "subject"], }) .then((response) => { console.log("Email response", response); response.list.forEach((e) => { if (this.state.session == null) return; const existing = this.state.session.emails[e.id]; if (existing !== undefined && existing.subject !== e.subject) { console.error( "Expectations violation: email ID is not unique within the server!", ); } this.state.session.emails[e.id] = e; this._triggerChange(); }); }) .catch((x) => { console.error("Failed to get email", emailId, x); }); } emailList(accountId: string, mailboxId: string, ids: Array) { if (this.jclient == null) return; this.jclient .email_query({ accountId: accountId, filter: { inMailbox: mailboxId }, }) .then((response) => { const mailbox = this.mailbox(accountId, mailboxId); if (mailbox == null) return; mailbox.emailIds = response.ids; this._triggerChange(); }) .catch(() => { console.error("Failed to get email list from mailbox", mailboxId); }); } mailbox(accountId: string, mailboxId: string): IMailbox | null { if (this.state.session == null) return null; const account = this.state.session.accounts[accountId]; if (account.mailboxes == null) return null; for (let i = 0; i < account.mailboxes.length; i++) { if (account.mailboxes[i].id === mailboxId) { return account.mailboxes[i]; } } return null; } mailboxList(accountId: string, ids: Array) { if (this.jclient == null) return; if (this.state.session == null) return; // We already populated the list if (this.state.session.accounts[accountId].mailboxes != null) return; this.jclient .mailbox_get({ accountId: accountId, ids: null, }) .then((response) => { if (this.state.session == null) return; const account = this.state.session.accounts[response.accountId]; const mailboxes: Array = []; response.list.forEach((m) => { mailboxes.push({ ...m, emailIds: null, emails: null, }); }); account.mailboxes = mailboxes; this._triggerChange(); }); } _triggerChange() { this.callbacks.forEach((c) => { c(); }); } _onSession() { console.log("Session received"); // For the type checker if (!this.jclient) return; const session = this.jclient.getSession(); this.state.session = { ...session, accounts: Object.fromEntries( Object.entries(session.accounts).map(([key, account]) => [ key, { ...account, id: key.toString(), mailboxes: null }, ]), ), emails: {}, }; if (!this.state.session) return; this._triggerChange(); } }