Cloudflare Email Workers を試す



# Email Workers とは

- [Route to Workers, automate your email processing](https://blog.cloudflare.com/announcing-route-to-workers/)

具体的な機能紹介は、上記のブログに書かれていますが、Email Routing のサービスとともに利用し、受信したメールを EmailEvent として Workers ロジックを経由して転送させることができます。

- [Betas · Cloudflare Workers docs](https://developers.cloudflare.com/workers/platform/betas/)

こちらを見ると、2022 年 3 月 3 日時点では Private Beta となっています。

![image-20230303152202344](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20230303152202344.png)

# 前提条件

まずは Email Routing を構成してある必要があります。

以下のドキュメントに沿って、ドメインを登録し、Email Routing を有効化しておきます。

- [Enable Email Routing · Cloudflare Email Routing docs](https://developers.cloudflare.com/email-routing/get-started/enable-email-routing/)

![image-20230303152651920](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20230303152651920.png)

# Email Workers の構成

- [Email Workers · Cloudflare Email Routing docs](https://developers.cloudflare.com/email-routing/email-workers/)

Email Workers を作成します。

![image-20230303153007178](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20230303153007178.png)

スターターテンプレートがあるので、Allowlist senders で進めます。

![image-20230303153103406](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20230303153103406.png)

Email Workers が作成できたら、コードを編集します。

![image-20230303153209907](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20230303153209907.png)

以下が今回使用するコードです。

- テンプレートデフォルトの `message.headers.get("from")` は `First Last <email@address>` となるため、if 条件分岐には `message.from` を使う

- `allowList` や `message.forward` は環境に応じて変更する

  - `message.forward` 先は Verified な宛先のみ対象にできる
（迷惑メールの温床とならないためにも当然ですね）

    > - Forward this email message to a verified destination address of the account. If you want, you can add extra headers to the email message. Only `X-*` headers are allowed.
    > - When the promise resolves, the message is confirmed to be forwarded to a verified destination address.

    ![image-20230303154016143](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20230303154016143.png)

- [EmailEvent](https://developers.cloudflare.com/workers/runtime-apis/email-event/#emailmessage-definition) に含まれる情報をログに表示する

```typescript:allowlist-email-worker.ts
export default {
  async email(message, env, ctx) {
    const allowList = ["abc@gmail.com"];
    console.log(`message from: ${message.from}`)
    console.log(`message to: ${message.to}`)
    console.log(`message subject: ${message.headers.get('subject')}`)
    console.log(`message date: ${message.headers.get('date')}`)
    console.log(`message message-id: ${message.headers.get('message-id')}`)
    console.log(`message received: ${message.headers.get('received')}`)
    console.log(`message headers: ${JSON.stringify([...message.headers])}`)
    const { value: messageRaw } = await message.raw.getReader().read()
    const messageRawJSON = new TextDecoder().decode(messageRaw)
    console.log(`message raw: ${messageRawJSON}`)
    console.log(`message rawSize: ${message.rawSize}`)
    if (allowList.indexOf(message.from) == -1) {
      message.setReject("Address not allowed");
    } else {
      await message.forward("test@corp.com");
    }
  }
}

/*
interface EmailMessage<Body = unknown> {
  readonly from: string;
  readonly to: string;
  readonly headers: Headers;
  readonly raw: ReadableStream;
  readonly rawSize: number;

  setReject(reason: String): void;
  forward(rcptTo: string, headers?: Headers): Promise<void>;
}
*/
```

# Route の構成

作成した Email Worker を Email Routing の受信アドレスの Action > Send to a Worker で指定します。

![image-20230303154307636](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20230303154307636.png)

# Email Workers を経由させたログ

Email Routing の受信アドレスにメールを送れば、`allowlist` の場合、 `message.forward` 先に送られることが確認できます。

```json
{
  "outcome": "ok",
  "scriptName": "allowlist-email-worker",
  "exceptions": [],
  "logs": [
    {
      "message": [
        "message from: abc@gmail.com"
      ],
      "level": "log",
      "timestamp": 1677775133400
    },
    {
      "message": [
        "message to: destination@your-email-routing-domain.com"
      ],
      "level": "log",
      "timestamp": 1677775133400
    },
    {
      "message": [
        "message subject: hello"
      ],
      "level": "log",
      "timestamp": 1677775133400
    },
    {
      "message": [
        "message date: Fri, 3 Mar 2023 01:38:41 +0900"
      ],
      "level": "log",
      "timestamp": 1677775133400
    },
    {
      "message": [
        "message message-id: <xxx@mail.gmail.com>"
      ],
      "level": "log",
      "timestamp": 1677775133400
    },
    {
      "message": [
        "message received: by mail-xxx.google.com with SMTP id xxx3.0 for <destination@your-email-routing-domain.com>; Thu, 02 Mar 2023 08:38:53 -0800 (PST)"
      ],
      "level": "log",
      "timestamp": 1677775133400
    },
    {
      "message": [
        "message headers: [
        [\"content-type\",\"multipart/alternative; boundary=\\\"xxx\\\"\"],
        [\"date\",\"Fri, 3 Mar 2023 01:38:41 +0900\"],[\"dkim-signature\",\"v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=xx; b=xx\"],
        [\"from\",\"First Last <abc@gmail.com>\"],
        [\"message-id\",\"<xxx@mail.gmail.com>\"],
        [\"mime-version\",\"1.0\"],
        [\"received\",\"by mail-xxx.google.com with SMTP id xxx3.0 for <destination@your-email-routing-domain.com>; Thu, 02 Mar 2023 08:38:53 -0800 (PST)\"],
        [\"subject\",\"hello\"],
        [\"to\",\"destination@your-email-routing-domain.com\"],
        [\"x-gm-message-state\",\"xxx\"],
        [\"x-google-dkim-signature\",\"v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=to:subject:message-id:date:from:mime-version:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=xx; b=xx\"],
        [\"x-google-smtp-source\",\"xxx\"],
        [\"x-received\",\"by xxx with SMTP id xxx.3.1677775132715; Thu, 02 Mar 2023 08:38:52 -0800 (PST)\"]]"
      ],
      "level": "log",
      "timestamp": 1677775133400
    },
    {
      "message": [
        "message raw: Received: from mail-xx.google.com (2607:f8b0:4864:20::1133)\r\n        
        by email.cloudflare.net (unknown) id LJvo9DIQ3xpI\r\n        
        for <destination@your-email-routing-domain.com>; Thu, 02 Mar 2023 16:38:53 +0000\r\n
        DKIM-Signature: v=1; a=rsa-sha256; d=email.cloudflare.net; s=2022; c=relaxed/relaxed; bh=xx; h=from:subject:date:to; t=1677775133; b=xx;\r\n
        Received: by mail-xx.google.com with SMTP id xx3.0\r\n        
        for <destination@your-email-routing-domain.com>; Thu, 02 Mar 2023 08:38:53 -0800 (PST)\r\n
        DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;\r\n        
        d=gmail.com; s=20210112;\r\n        
        h=to:subject:message-id:date:from:mime-version:from:to:cc:subject\r\n         
        :date:message-id:reply-to;\r\n        
        bh=xx=;\r\n        
        b=xx==\r\n
        X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;\r\n        
        d=1e100.net; s=20210112;\r\n        
        h=to:subject:message-id:date:from:mime-version:x-gm-message-state\r\n         
        :from:to:cc:subject:date:message-id:reply-to;\r\n        
        bh=xx\r\n        
        b=xx\r\n
        X-Gm-Message-State: xx\r\n
        X-Google-Smtp-Source: xx\r\n
        X-Received: by xx with SMTP id\r\n 
        xx.3.1677775132715; Thu, 02\r\n 
        Mar 2023 08:38:52 -0800 (PST)\r\n
        MIME-Version: 1.0\r\n
        From: First Last <abc@gmail.com>\r\n
        Date: Fri, 3 Mar 2023 01:38:41 +0900\r\n
        Message-ID: <xxx@mail.gmail.com>\r\n
        Subject: hello\r\n
        To: destination@your-email-routing-domain.com\r\n
        Content-Type: multipart/alternative; boundary=\"xx\"\r\n
        \r\n
        --000000000000cc700b05f5ed7612\r\n
        Content-Type: text/plain; charset=\"UTF-8\"\r\n
        \r\n
        hello world\r\n
        \r\n
        --000000000000cc700b05f5ed7612\r\n
        Content-Type: text/html; charset=\"UTF-8\"\r\n
        \r\n
        <div dir=\"ltr\">hello world<br></div>\r\n
        \r\n
        --000000000000cc700b05f5ed7612--\r\n"
      ],
      "level": "log",
      "timestamp": 1677775133400
    },
    {
      "message": [
        "message rawSize: 3224"
      ],
      "level": "log",
      "timestamp": 1677775133400
    }
  ],
  "eventTimestamp": 1677775133398,
  "event": {
    "rawSize": 3224,
    "rcptTo": "destination@your-email-routing-domain.com",
    "mailFrom": "abc@gmail.com"
  },
  "id": 4
}
```

# `message.setReject("Address not allowed")` の場合

以下のようなエラーメールが送信者に返ってきます。

> - Reject this email message by returning a permanent SMTP error back to the connecting client, including the given reason.

![image-20230303014246742](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20230303014246742.png)

# not verified なメールアドレスを転送先とした場合

`message.forward` 先は Verified な宛先のみ対象にでき、以下のエラーとなります。

```json
{
  "outcome": "exception",
  "scriptName": "allowlist-email-worker",
  "exceptions": [
    {
      "name": "Error",
      "message": "destination address not verified",
      "timestamp": 1677774635838
    }
  ],
  ...
}

```

![image-20230303013408494](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20230303013408494.png)

# 受信したサンプルメール
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/4778829d-967b-5782-9c33-11eb1d6b914a.png)

# 感想

- Allowlist, Blocklist, メール受信を Webhook で通知、くらいであれば簡単にできる
- X- メールヘッダの追加等も簡単にできる（`X-My-Email-Worker-Processed`）
- `messageRaw` の扱いは、以下のコード例にあるパーサーを使うとより効率的
    - [edevil/email_worker_parser: An example of a worker that parses an email](https://github.com/edevil/email_worker_parser/tree/main)

```
{
  "outcome": "ok",
  "scriptName": "email_worker_parser",
  "exceptions": [],
  "logs": [
    {
      "message": [
        "Mail subject: ",
        "Test"
      ],
      "level": "log",
      "timestamp": 1684431951396
    },
    {
      "message": [
        "HTML version of Email: ",
        "<div dir=\"ltr\">Hello, World!<br></div>\n\n"
      ],
      "level": "log",
      "timestamp": 1684431951396
    },
    {
      "message": [
        "Text version of Email: ",
        "Hello, World!\n\n"
      ],
      "level": "log",
      "timestamp": 1684431951396
    },
    {
      "message": [
        "No attachments"
      ],
      "level": "log",
      "timestamp": 1684431951396
    }
  ],
  "eventTimestamp": 1684431951384,
  "event": {
    "rawSize": 5405,
    "rcptTo": "receiver@example.com",
    "mailFrom": "sender@example.com"
  },
  "id": 0
}
```