/* * 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, IEmailStub, IMailbox, ISession } from "./types"; type Callback = () => void; export interface IAuth { email: string; password: string; } export interface ClientState { inFlight: { emailContents: Set; emailStubs: Set; mailboxes: Set; }; session: ISession | null; } export default class Client { // All objects which currently are listening for changes callbacks: Array = []; jclient: jmapclient.Client | null = null; state: ClientState = { inFlight: { emailContents: new Set(), emailStubs: new Set(), mailboxes: new Set(), }, session: null, }; // 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]; } // 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; } // Ensure we have the full email content ensureEmailContent(accountId: string, emailId: string) { if (this.state.session == null) return; const existing = this.state.session.emails[emailId]; if (existing != null) return; this.emailGetContent(accountId, emailId); } // Ensure we have the email summary ensureEmailStub(accountId: string, emailId: string) { if (this.state.session == null) return; const existing = this.state.session.emailStubs[emailId]; if (existing != null) return; this.emailGetStub(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); } email(emailId: string): IEmail | null { if (this.state.session == null) return null; return this.state.session.emails[emailId]; } emailGetContent(accountId: string, emailId: string) { if (this.jclient == null) return; if (this.state.session == null) return; if (this.state.inFlight.emailContents.has(emailId)) return; this.state.inFlight.emailContents.add(emailId); const msg = "Email get content"; this.jclient .email_get({ accountId: accountId, ids: [emailId], fetchAllBodyValues: true, }) .then((response) => { console.log(msg, "response", response); response.list.forEach((e) => { 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, id: e.id, }; this._triggerChange(msg + e.id); }); }) .catch((x) => { console.error("Failed to get email content", emailId, x); }) .finally(() => { this.state.inFlight.emailContents.delete(emailId); }); } emailGetStub(accountId: string, emailId: string) { if (this.jclient == null) return; if (this.state.session == null) return; if (this.state.inFlight.emailStubs.has(emailId)) return; this.state.inFlight.emailStubs.add(emailId); const msg = "Email get summary"; this.jclient .email_get({ accountId: accountId, ids: [emailId], properties: ["from", "receivedAt", "subject"], }) .then((response) => { console.log(msg, "response", response); response.list.forEach((e) => { if (this.state.session == null) return; const existing = this.state.session.emailStubs[e.id]; if (existing !== undefined && existing.subject !== e.subject) { console.error( "Expectations violation: email ID is not unique within the server!", ); } this.state.session.emailStubs[e.id] = { from: e.from, id: e.id, receivedAt: e.receivedAt, subject: e.subject, }; this._triggerChange(msg + e.id); }); }) .catch((x) => { console.error("Failed to get email stub", emailId, x); }) .finally(() => { this.state.inFlight.emailStubs.delete(emailId); }); } emailList(accountId: string, mailboxId: string, ids: Array) { if (this.jclient == null) return; this.jclient .email_query({ accountId: accountId, filter: { inMailbox: mailboxId }, sort: [ { property: "receivedAt", isAscending: false, }, ], }) .then((response) => { const mailbox = this.mailbox(accountId, mailboxId); if (mailbox == null) return; mailbox.emailIds = response.ids; this._triggerChange("Email list " + mailboxId); }) .catch(() => { console.error("Failed to get email list from mailbox", mailboxId); }); } emailStub(emailId: string): IEmailStub | null { if (this.state.session == null) return null; return this.state.session.emailStubs[emailId]; } 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, }); }); account.mailboxes = mailboxes; this._triggerChange("Mailboxes " + accountId); }); } onChange(f: Callback) { this.callbacks.push(f); } _triggerChange(msg: string) { console.log("Client change", msg); 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: {}, emailStubs: {}, }; if (!this.state.session) return; this._triggerChange("Session"); } }