aboutsummaryrefslogtreecommitdiff
path: root/deno/mail/aws/fetch.ts
diff options
context:
space:
mode:
Diffstat (limited to 'deno/mail/aws/fetch.ts')
-rw-r--r--deno/mail/aws/fetch.ts136
1 files changed, 136 insertions, 0 deletions
diff --git a/deno/mail/aws/fetch.ts b/deno/mail/aws/fetch.ts
new file mode 100644
index 0000000..2154972
--- /dev/null
+++ b/deno/mail/aws/fetch.ts
@@ -0,0 +1,136 @@
+import {
+ CopyObjectCommand,
+ DeleteObjectCommand,
+ GetObjectCommand,
+ ListObjectsV2Command,
+ NoSuchKey,
+ S3Client,
+ S3ClientConfig,
+} from "@aws-sdk/client-s3";
+
+import { DateUtils } from "@crupest/base";
+
+import { Mail } from "../mail.ts";
+import { MailDeliverer } from "../mail.ts";
+
+export class LiveMailNotFoundError extends Error {}
+
+async function s3MoveObject(
+ client: S3Client,
+ bucket: string,
+ path: string,
+ newPath: string,
+): Promise<void> {
+ const copyCommand = new CopyObjectCommand({
+ Bucket: bucket,
+ Key: newPath,
+ CopySource: `${bucket}/${path}`,
+ });
+ await client.send(copyCommand);
+
+ const deleteCommand = new DeleteObjectCommand({
+ Bucket: bucket,
+ Key: path,
+ });
+ await client.send(deleteCommand);
+}
+
+const AWS_SES_S3_SETUP_TAG = "AMAZON_SES_SETUP_NOTIFICATION";
+
+export class AwsMailFetcher {
+ readonly #livePrefix = "mail/live/";
+ readonly #archivePrefix = "mail/archive/";
+ readonly #s3;
+ readonly #bucket;
+
+ constructor(aws: S3ClientConfig, bucket: string) {
+ this.#s3 = new S3Client(aws);
+ this.#bucket = bucket;
+ }
+
+ async listLiveMails(): Promise<string[]> {
+ const listCommand = new ListObjectsV2Command({
+ Bucket: this.#bucket,
+ Prefix: this.#livePrefix,
+ });
+ const res = await this.#s3.send(listCommand);
+
+ if (res.Contents == null) {
+ console.warn("S3 API returned null Content.");
+ return [];
+ }
+
+ const result: string[] = [];
+ for (const object of res.Contents) {
+ if (object.Key == null) {
+ console.warn("S3 API returned null Key.");
+ continue;
+ }
+
+ if (object.Key.endsWith(AWS_SES_S3_SETUP_TAG)) continue;
+
+ result.push(object.Key.slice(this.#livePrefix.length));
+ }
+ return result;
+ }
+
+ async deliverLiveMail(
+ logTag: string,
+ s3Key: string,
+ deliverer: MailDeliverer,
+ recipients?: string[],
+ ) {
+ console.info(logTag, `Fetching live mail ${s3Key}...`);
+ const mailPath = `${this.#livePrefix}${s3Key}`;
+ const command = new GetObjectCommand({
+ Bucket: this.#bucket,
+ Key: mailPath,
+ });
+
+ let rawMail;
+
+ try {
+ const res = await this.#s3.send(command);
+ if (res.Body == null) {
+ throw new Error("S3 API returns a null body.");
+ }
+ rawMail = await res.Body.transformToString();
+ } catch (cause) {
+ if (cause instanceof NoSuchKey) {
+ const message =
+ `Live mail ${s3Key} is not found. Perhaps already delivered?`;
+ console.error(message, cause);
+ throw new LiveMailNotFoundError(message);
+ }
+ throw cause;
+ }
+
+ const mail = new Mail(rawMail);
+ await deliverer.deliver({ mail, recipients });
+
+ const { date } = new Mail(rawMail).parsed;
+ const dateString = date != null
+ ? DateUtils.toFileNameString(date, true)
+ : "invalid-date";
+ const newPath = `${this.#archivePrefix}${dateString}/${s3Key}`;
+
+ console.info(logTag, `Archiving live mail ${s3Key} to ${newPath}...`);
+ await s3MoveObject(this.#s3, this.#bucket, mailPath, newPath);
+
+ console.info(logTag, `Done deliver live mail ${s3Key}.`);
+ }
+
+ async recycleLiveMails(deliverer: MailDeliverer) {
+ console.info("Begin to recycle live mails...");
+ const mails = await this.listLiveMails();
+ console.info(`Found ${mails.length} live mails`);
+ let counter = 1;
+ for (const s3Key of mails) {
+ await this.deliverLiveMail(
+ `[${counter++}/${mails.length}]`,
+ s3Key,
+ deliverer,
+ );
+ }
+ }
+}