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);
}
}
|