drishti/src/client/Client.tsx

268 lines
7.2 KiB
TypeScript
Raw Normal View History

/*
* 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<string>;
emailStubs: Set<string>;
mailboxes: Set<string>;
};
session: ISession | null;
}
export default class Client {
// All objects which currently are listening for changes
callbacks: Array<Callback> = [];
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],
})
.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: ["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] = {
id: e.id,
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<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;
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<string>) {
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<IMailbox> = [];
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");
}
}