// Import the assert function from the "assert" module
import { assert } from "https://deno.land/[email protected]/testing/asserts.ts";
interface EmailAddress {
email: string;
}
interface EmailDetails {
from: string;
to: string;
cc: string;
subject: string;
messageBody: string;
mailboxId: string;
}
/**
* TinyJMAPClient class provides methods to interact with a JMAP server.
*/
export class TinyJMAPClient {
private hostname: string;
private username: string;
private token: string;
private session: any;
private api_url: string | null = null;
private account_id: string | null = null;
private identity_id: string | null = null;
/**
* Constructor initializes the client with basic settings.
*
* @param hostname - JMAP server hostname.
* @param username - Username for authentication.
* @param token - Authentication token.
*/
constructor(hostname: string, username: string, token: string) {
assert(hostname.length > 0);
assert(username.length > 0);
assert(token.length > 0);
this.hostname = hostname;
this.username = username;
this.token = token;
this.session = null;
}
/**
* Retrieve the JMAP session.
*
* @returns A promise that resolves with the session object.
*/
async get_session() {
if (this.session) {
return this.session;
}
const r = await fetch(`https://${this.hostname}/.well-known/jmap`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${this.token}`,
},
});
if (!r.ok) {
throw new Error(`Failed to fetch JMAP session: ${r.status} ${r.statusText}`);
}
this.session = await r.json();
this.api_url = this.session.apiUrl;
return this.session;
}
/**
* Fetch the account ID for JMAP operations.
*
* @returns A promise that resolves with the account ID.
*/
async get_account_id() {
if (this.account_id) {
return this.account_id;
}
const session = await this.get_session();
const account_id = session.primaryAccounts["urn:ietf:params:jmap:mail"];
this.account_id = account_id;
return account_id;
}
/**
* Retrieve the identity ID.
*
* @returns A promise that resolves with the identity ID.
*/
async get_identity_id() {
if (this.identity_id) {
return this.identity_id;
}
const identity_res = await this.make_jmap_call({
using: [
"urn:ietf:params:jmap:core",
"urn:ietf:params:jmap:submission",
],
methodCalls: [
["Identity/get", { accountId: await this.get_account_id() }, "i"],
],
});
const identity_id = identity_res.methodResponses[0][1]["list"].find(
(i: any) => i.email === this.username
).id;
this.identity_id = identity_id;
return this.identity_id;
}
/**
* Perform a JMAP API call.
*
* @param call - The API call details.
* @returns A promise that resolves with the API call result.
*/
async make_jmap_call(call: any) {
const res = await fetch(this.api_url as string, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${this.token}`,
},
body: call,
});
if (!res.ok) {
throw new Error(`Failed to make JMAP API call: ${res.status} ${res.statusText}`);
}
return await res.json();
}
/**
* Create a new email draft and optionally send it.
*
* @param emailDetails - Object containing details of the email.
* @param shouldSend - Flag indicating whether to send the email immediately. Default is `false`.
* @returns A promise that resolves once the operation is complete.
*
* @example
* ```typescript
* const emailDetails: EmailDetails = {
* from: "sender@example.com",
* to: "recipient@example.com",
* subject: "Test Email",
* messageBody: "This is a test email body.",
* mailboxId: "your_mailbox_id_here",
* };
*
* await client.create_draft_email(emailDetails, true); // Sends the email
* await client.create_draft_email(emailDetails, false); // Only saves as draft
* ```
*/
async create_draft_email(emailDetails: EmailDetails, shouldSend: boolean = false): Promise<void> {
const { from, to, subject, messageBody, mailboxId } = emailDetails;
const accountId = await this.get_account_id();
const identityId = await this.get_identity_id();
const draftObject = {
from: [{ email: from}],
to: [{ email: to}],
subject,
keywords: shouldSend ? {} : { $draft: true },
mailboxIds: { [mailboxId]: true },
bodyValues: { body: { value: messageBody, charset: "utf-8"}},
textBody: [{ partId: "body", type: "text/plain"}]
};
const res = await this.make_jmap_call(JSON.stringify({
using: [
"urn:ietf:params:jmap:core",
"urn:ietf:params:jmap:mail",
"urn:ietf:params:jmap:submission",
],
methodCalls: [
["Email/set", { accountId, create: { draft: draftObject } }, "a"],
[
"EmailSubmission/set",
{
accountId,
onSuccessDestroyEmail: ["#sendIt"],
create: { sendIt: { emailId: "#draft", identityId } },
},
"b",
],
],
}));
if (!res.ok) {
throw new Error(`Failed to send the created Draft using JMAP': ${res.status} ${res.statusText}`)
}
return await res.json();
}
}
Submitted by kryptonian283 958 days ago