2024-08-27 22:49:48 -07:00
|
|
|
/*
|
|
|
|
* 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";
|
|
|
|
|
2024-08-28 09:21:31 -07:00
|
|
|
import { IAccount, IEmail, IMailbox, ISession } from "./types";
|
2024-08-27 22:49:48 -07:00
|
|
|
|
|
|
|
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<Callback> = [];
|
|
|
|
jclient: jmapclient.Client | null = null;
|
|
|
|
state: ClientState = {
|
|
|
|
session: null,
|
|
|
|
};
|
|
|
|
|
|
|
|
onChange(f: Callback) {
|
|
|
|
this.callbacks.push(f);
|
|
|
|
}
|
|
|
|
|
2024-08-28 09:21:31 -07:00
|
|
|
// 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, []);
|
|
|
|
}
|
|
|
|
|
2024-08-27 22:49:48 -07:00
|
|
|
// Ensure that login happens. If this is called many times, only login once.
|
|
|
|
ensureLogin(auth: IAuth) {
|
|
|
|
if (this.jclient != null) return;
|
|
|
|
this.doLogin(auth);
|
|
|
|
}
|
2024-08-28 09:21:31 -07:00
|
|
|
|
2024-08-27 22:49:48 -07:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2024-08-28 09:21:31 -07:00
|
|
|
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;
|
2024-08-28 10:16:40 -07:00
|
|
|
this._triggerChange("Email " + e.id);
|
2024-08-28 09:21:31 -07:00
|
|
|
});
|
|
|
|
})
|
|
|
|
.catch((x) => {
|
|
|
|
console.error("Failed to get email", emailId, x);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-08-28 00:58:32 -07:00
|
|
|
emailList(accountId: string, mailboxId: string, ids: Array<string>) {
|
|
|
|
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;
|
2024-08-28 10:16:40 -07:00
|
|
|
this._triggerChange("Email list " + mailboxId);
|
2024-08-28 00:58:32 -07:00
|
|
|
})
|
|
|
|
.catch(() => {
|
2024-08-28 09:21:31 -07:00
|
|
|
console.error("Failed to get email list from mailbox", mailboxId);
|
2024-08-28 00:58:32 -07:00
|
|
|
});
|
|
|
|
}
|
2024-08-27 23:22:11 -07:00
|
|
|
|
2024-08-28 00:58:32 -07:00
|
|
|
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;
|
|
|
|
}
|
2024-08-27 22:49:48 -07:00
|
|
|
mailboxList(accountId: string, ids: Array<string>) {
|
|
|
|
if (this.jclient == null) return;
|
2024-08-28 00:58:32 -07:00
|
|
|
if (this.state.session == null) return;
|
|
|
|
// We already populated the list
|
|
|
|
if (this.state.session.accounts[accountId].mailboxes != null) return;
|
2024-08-27 22:49:48 -07:00
|
|
|
this.jclient
|
|
|
|
.mailbox_get({
|
|
|
|
accountId: accountId,
|
|
|
|
ids: null,
|
|
|
|
})
|
|
|
|
.then((response) => {
|
|
|
|
if (this.state.session == null) return;
|
|
|
|
const account = this.state.session.accounts[response.accountId];
|
2024-08-27 23:22:11 -07:00
|
|
|
const mailboxes: Array<IMailbox> = [];
|
|
|
|
response.list.forEach((m) => {
|
|
|
|
mailboxes.push({
|
|
|
|
...m,
|
2024-08-28 00:58:32 -07:00
|
|
|
emailIds: null,
|
|
|
|
emails: null,
|
2024-08-27 23:22:11 -07:00
|
|
|
});
|
|
|
|
});
|
|
|
|
account.mailboxes = mailboxes;
|
2024-08-28 10:16:40 -07:00
|
|
|
this._triggerChange("Mailboxes " + accountId);
|
2024-08-27 22:49:48 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-08-28 10:16:40 -07:00
|
|
|
_triggerChange(msg: string) {
|
|
|
|
console.log("Client change", msg);
|
2024-08-27 22:49:48 -07:00
|
|
|
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,
|
2024-08-28 00:58:32 -07:00
|
|
|
{ ...account, id: key.toString(), mailboxes: null },
|
2024-08-27 22:49:48 -07:00
|
|
|
]),
|
|
|
|
),
|
2024-08-28 09:21:31 -07:00
|
|
|
emails: {},
|
2024-08-27 22:49:48 -07:00
|
|
|
};
|
|
|
|
if (!this.state.session) return;
|
2024-08-28 10:16:40 -07:00
|
|
|
this._triggerChange("Session");
|
2024-08-27 22:49:48 -07:00
|
|
|
}
|
|
|
|
}
|