aboutsummaryrefslogtreecommitdiff
path: root/services/docker/mail-server/aws-sendmail/mail.ts
blob: de7942fd41b4d57db6b10c25775f0bd2b06d889c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { encodeBase64 } from "@std/encoding/base64";
import { getLogger } from "./logger.ts";

export type MailDeliverStateNotSent = { kind: "not-sent" };
export type MailDeliverStateDelivered = {
  kind: "delivered";
  deliverer: MailDeliverer;
  error?: Error;
};
export type MailDeliverStateError = {
  kind: "error";
  deliverer: MailDeliverer;
  error: Error;
};
export type MailDeliverState =
  | MailDeliverStateNotSent
  | MailDeliverStateDelivered
  | MailDeliverStateError;

class MailDeliverError extends Error {
  constructor(
    message: string,
    options: ErrorOptions,
    public readonly mail: Mail,
  ) {
    super(message, options);
  }
}

export class Mail {
  messageId: string | null = null;
  awsMessageId: string | null = null;
  deliverState: MailDeliverState = { kind: "not-sent" };

  constructor(public raw: string) {}

  setDelivered(deliverer: MailDeliverer, error?: Error) {
    this.deliverState = { "kind": "delivered", deliverer, error };
  }

  throwDeliverError(
    deliverer: MailDeliverer,
    reason: string,
    cause?: unknown,
  ): never {
    const error = new MailDeliverError(
      `Failed to deliver mail to ${deliverer.destination}: ${reason}`,
      { cause },
      this,
    );
    this.deliverState = { "kind": "error", deliverer, error };
    throw error;
  }

  encodeUtf8(): Uint8Array {
    const utf8Encoder = new TextEncoder();
    // TODO: A problem here is if mail is VERY long, this will block for a long time.
    // Maybe some task queue can be used.
    return utf8Encoder.encode(this.raw);
  }

  getRawBase64(): string {
    return encodeBase64(this.raw);
  }

  appendHeaders(
    rawMail: string,
    headers: [key: string, value: string][],
  ): string {
    const separatorMatch = rawMail.match(/(\r\n|\n)(\r\n|\n)/);
    if (separatorMatch == null) {
      throw new Error(
        "No header/body separator (2 successive EOLs) found. Cannot append headers.",
      );
    }

    if (separatorMatch[1] !== separatorMatch[2]) {
      getLogger().warn("Different EOLs (\\r\\n and \\n) found in mail!");
    }

    const headerStr = headers.map(([k, v]) => `${k}: ${v}${separatorMatch[1]}`)
      .join("");
    const endOfHeadersIndex = separatorMatch.index! + separatorMatch[1].length;
    return rawMail.slice(0, endOfHeadersIndex) + headerStr +
      rawMail.slice(endOfHeadersIndex);
  }
}

type MailDeliverHook = (mail: Mail) => Promise<void>;

export abstract class MailDeliverer {
  preHooks: MailDeliverHook[] = [];
  postHooks: MailDeliverHook[] = [];

  constructor(public readonly destination: string) {}

  protected doPrepare(_mail: Mail): Promise<void> {
    return Promise.resolve();
  }
  protected abstract doDeliver(mail: Mail): Promise<void>;
  protected doFinalize(_mail: Mail): Promise<void> {
    return Promise.resolve();
  }

  async deliverRaw(raw: string): Promise<void> {
    const mail = new Mail(raw);
    await this.deliver(mail);
  }

  async deliver(mail: Mail): Promise<void> {
    this.doPrepare(mail);

    for (const hook of this.preHooks) {
      await hook(mail);
    }

    await this.doDeliver(mail);

    if (mail.deliverState.kind === "not-sent") {
      mail.setDelivered(this);
    }

    for (const hook of this.postHooks) {
      await hook(mail);
    }

    await this.doFinalize(mail);
  }
}