277 lines
7.4 KiB
TypeScript
277 lines
7.4 KiB
TypeScript
/*
|
|
* 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],
|
|
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<string>) {
|
|
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<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");
|
|
}
|
|
}
|