0

API client

by
Published Oct 26, 2023
Script jmap
  • Submitted by kryptonian283 Deno
    Created 958 days ago
    1
    // Import the assert function from the "assert" module
    2
    import { assert } from "https://deno.land/[email protected]/testing/asserts.ts";
    3
    
    
    4
    interface EmailAddress {
    5
      email: string;
    6
    }
    7
    
    
    8
    interface EmailDetails {
    9
      from: string;
    10
      to: string;
    11
      cc: string;
    12
      subject: string;
    13
      messageBody: string;
    14
      mailboxId: string;
    15
    }
    16
    
    
    17
    /**
    18
     * TinyJMAPClient class provides methods to interact with a JMAP server.
    19
     */
    20
    export class TinyJMAPClient {
    21
      private hostname: string;
    22
      private username: string;
    23
      private token: string;
    24
      private session: any;
    25
      private api_url: string | null = null;
    26
      private account_id: string | null = null;
    27
      private identity_id: string | null = null;
    28
    
    
    29
      /**
    30
       * Constructor initializes the client with basic settings.
    31
       *
    32
       * @param hostname - JMAP server hostname.
    33
       * @param username - Username for authentication.
    34
       * @param token - Authentication token.
    35
       */
    36
      constructor(hostname: string, username: string, token: string) {
    37
        assert(hostname.length > 0);
    38
        assert(username.length > 0);
    39
        assert(token.length > 0);
    40
    
    
    41
        this.hostname = hostname;
    42
        this.username = username;
    43
        this.token = token;
    44
        this.session = null;
    45
      }
    46
    
    
    47
      /**
    48
       * Retrieve the JMAP session.
    49
       *
    50
       * @returns A promise that resolves with the session object.
    51
       */
    52
      async get_session() {
    53
        if (this.session) {
    54
          return this.session;
    55
        }
    56
    
    
    57
        const r = await fetch(`https://${this.hostname}/.well-known/jmap`, {
    58
          method: "GET",
    59
          headers: {
    60
            "Content-Type": "application/json",
    61
            "Authorization": `Bearer ${this.token}`,
    62
          },
    63
        });
    64
    
    
    65
        if (!r.ok) {
    66
          throw new Error(`Failed to fetch JMAP session: ${r.status} ${r.statusText}`);
    67
        }
    68
    
    
    69
        this.session = await r.json();
    70
        this.api_url = this.session.apiUrl;
    71
        return this.session;
    72
      }
    73
    
    
    74
      /**
    75
       * Fetch the account ID for JMAP operations.
    76
       *
    77
       * @returns A promise that resolves with the account ID.
    78
       */
    79
      async get_account_id() {
    80
        if (this.account_id) {
    81
          return this.account_id;
    82
        }
    83
    
    
    84
        const session = await this.get_session();
    85
        const account_id = session.primaryAccounts["urn:ietf:params:jmap:mail"];
    86
        this.account_id = account_id;
    87
        return account_id;
    88
      }
    89
    
    
    90
      /**
    91
       * Retrieve the identity ID.
    92
       *
    93
       * @returns A promise that resolves with the identity ID.
    94
       */
    95
      async get_identity_id() {
    96
        if (this.identity_id) {
    97
          return this.identity_id;
    98
        }
    99
    
    
    100
        const identity_res = await this.make_jmap_call({
    101
          using: [
    102
            "urn:ietf:params:jmap:core",
    103
            "urn:ietf:params:jmap:submission",
    104
          ],
    105
          methodCalls: [
    106
            ["Identity/get", { accountId: await this.get_account_id() }, "i"],
    107
          ],
    108
        });
    109
    
    
    110
        const identity_id = identity_res.methodResponses[0][1]["list"].find(
    111
          (i: any) => i.email === this.username
    112
        ).id;
    113
    
    
    114
        this.identity_id = identity_id;
    115
        return this.identity_id;
    116
      }
    117
      
    118
      /**
    119
       * Perform a JMAP API call.
    120
       *
    121
       * @param call - The API call details.
    122
       * @returns A promise that resolves with the API call result.
    123
       */
    124
      async make_jmap_call(call: any) {
    125
        const res = await fetch(this.api_url as string, {
    126
          method: "POST",
    127
          headers: {
    128
            "Content-Type": "application/json",
    129
            "Authorization": `Bearer ${this.token}`,
    130
          },
    131
          body: call,
    132
        });
    133
    
    
    134
        if (!res.ok) {
    135
          throw new Error(`Failed to make JMAP API call: ${res.status} ${res.statusText}`);
    136
        }
    137
    
    
    138
        return await res.json();
    139
      }
    140
    
    
    141
      
    142
      /**
    143
       * Create a new email draft and optionally send it.
    144
       *
    145
       * @param emailDetails - Object containing details of the email.
    146
       * @param shouldSend - Flag indicating whether to send the email immediately. Default is `false`.
    147
       * @returns A promise that resolves once the operation is complete.
    148
       *
    149
       * @example
    150
       * ```typescript
    151
       * const emailDetails: EmailDetails = {
    152
       *   from: "sender@example.com",
    153
       *   to: "recipient@example.com",
    154
       *   subject: "Test Email",
    155
       *   messageBody: "This is a test email body.",
    156
       *   mailboxId: "your_mailbox_id_here",
    157
       * };
    158
       *
    159
       * await client.create_draft_email(emailDetails, true);  // Sends the email
    160
       * await client.create_draft_email(emailDetails, false); // Only saves as draft
    161
       * ```
    162
       */
    163
      async create_draft_email(emailDetails: EmailDetails, shouldSend: boolean = false): Promise<void> {
    164
        const { from, to, subject, messageBody, mailboxId } = emailDetails;
    165
        const accountId = await this.get_account_id();
    166
        const identityId = await this.get_identity_id();
    167
    
    
    168
        const draftObject = {
    169
          from: [{ email: from}],
    170
          to: [{ email: to}],
    171
          subject,
    172
          keywords: shouldSend ? {} : { $draft: true },
    173
          mailboxIds: { [mailboxId]: true },
    174
          bodyValues: { body: { value: messageBody, charset: "utf-8"}},
    175
          textBody: [{ partId: "body", type: "text/plain"}]
    176
        };
    177
    
    
    178
    
    
    179
        const res = await this.make_jmap_call(JSON.stringify({
    180
          using: [
    181
            "urn:ietf:params:jmap:core",
    182
            "urn:ietf:params:jmap:mail",
    183
            "urn:ietf:params:jmap:submission",
    184
          ],
    185
          methodCalls: [
    186
            ["Email/set", { accountId, create: { draft: draftObject } }, "a"],
    187
            [
    188
              "EmailSubmission/set",
    189
              {
    190
                accountId,
    191
                onSuccessDestroyEmail: ["#sendIt"],
    192
                create: { sendIt: { emailId: "#draft", identityId } },
    193
              },
    194
              "b",
    195
            ],
    196
          ],
    197
        }));
    198
    
    
    199
        if (!res.ok) {
    200
          throw new Error(`Failed to send the created Draft using JMAP': ${res.status} ${res.statusText}`)
    201
        }
    202
    
    
    203
        return await res.json();
    204
      }
    205
    }
    206