Cloudflare Email WorkersでSlack通知するやつをオンラインCode Editor上で作りたかった

## Cloudflare Email Workers

CloudflareにはEmail Routingというのがあり、その中にはEmail Workersというのがあります。文字通り、メールを受け取ってプログラムで処理するやつです。

https://developers.cloudflare.com/email-routing/email-workers/

しかもこれが開発環境とか用意しなくてもブラウザだけで結構簡単に作れそうな気がしたので、作ってみました。（つくれませんでした）

:::note
Cloudflare Workersは本記事執筆時点でベータ版です。仕様が変わることがあります。
:::

## メールからSlack通知の実装
Email WorkersはCloudflareのWeb管理画面上のEmail→Email Routing→Email Workersから作成します。
作成画面に進むとstarterとしていくつかサンプルコードが用意されており、その中にはSlack通知もあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/cffb5f09-1926-6645-035b-9191115fdf1d.png)

さらに、Web上で動作する簡単なコードエディターを使って編集できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/f3c77366-929f-4fbd-71ac-222bef7000a7.png)

このスターターコードを使って、オンラインコードエディター上で通知先のURLなどをさくさくっと書き換えれば終わりそうです。やってみましょう。

### 実際のコード
...そんな甘い話ではなくて、実際にスターターコードを見てみると件名や宛先などヘッダーに載っている情報を通知するのみです。肝心の本文が取得できないので、これをSlackに通知してもあまり意味がありません。

本文を取得してみましょう。本文を取得する方法は[EmailMessage.rawからReadableStreamオブジェクトを取得](https://developers.cloudflare.com/email-routing/email-workers/runtime-api/#emailmessage-definition)してメール本文の生データを[RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322)とかに従ってパースすればOKです。作ってみましょう。

```javascript
export default {
  async email(message, env, ctx) {
    const SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/XXXXXXXX あなたのSlackのWeb Hook URL XXXXXXXX';
    const emailText = await messageToText(message);

    const data = {
      "username": `${message.from} からメール`,
      "channel": '#チャンネル名',
      "text": `件名:${message.headers.get('subject')}\n本文:\n${emailText}`,
    };

    await fetch(SLACK_WEBHOOK_URL, {
      headers: { 'Content-Type': 'application/json; charset=utf-8' },
      method: 'POST',
      body: JSON.stringify(data),
    });
  }
}

async function messageToText(message) {
  try {
    const rawEmail = await streamToArrayBuffer(message.raw, message.rawSize);
    return parseEmail(rawEmail);
  } catch(e) {
    return `${e.message}`;
  }
  return 'no text';
}

// メールをパースするメインの関数
function parseEmail(arrayBuffer) {
  const emailText = String.fromCharCode.apply('', new Uint16Array(arrayBuffer));
  const parts = emailText.split('\r\n\r\n');
  const mailBody = parts.slice(1).join('\r\n\r\n');
  return decodeBase64Email(mailBody);
}

// ※この関数はChatGPTを使って生成したものをベースに修正した
function decodeBase64Email(emailContent) {
  let emailText = "";
  // マルチパートメッセージ対応。本来は区切り文字列をヘッダーから読み取る
  const parts = emailContent.split('--');

  parts.forEach(part => {
    // text/plain base64のみ対応する
    let textPlain = false;
    let base64 = false;
    part.split('\r\n').forEach(line => {
      if (line.includes('Content-Type: text/plain')) { textPlain = true };
      if (line.includes('Content-Transfer-Encoding: base64')) { base64 = true };
    });
    // 本文該当箇所をデコード
    if (textPlain && base64) {
      const body = part.split('\r\n\r\n').slice(1);
      const decodedArray = new Uint8Array(Array.prototype.map.call(atob(body), c => c.charCodeAt()));
      const decoded = new TextDecoder().decode(decodedArray);
      emailText += decoded;
    }
  });
  return emailText;
}

// https://github.com/edevil/email_worker_parser/blob/71a2855e209d5fb028fe6d96c52fbfbf0d580d51/src/index.js#L5C1-L19C2
async function streamToArrayBuffer(stream, streamSize) {
  let result = new Uint8Array(streamSize);
  let bytesRead = 0;
  const reader = stream.getReader();
  while (true) {
    const { done, value } = await reader.read();
    if (done) {
      break;
    }
    result.set(value, bytesRead);
    bytesRead += value.length;
  }
  return result;
}
```

これで作ったWorkerを適当なメールアドレスにルーティングして、Gmail等から適当なテキストメールを送信すると、指定したSlackチャンネルに通知が来ると思います。多分。

### 何このコード簡単じゃないんだけど
スターターのコードは10行ぐらいしか無いシンプルなコードだったので簡単そうに見えましたが、実際に完成した上記のコードはちょっと長くなっています。こんなはずではありませんでした。しかしCloudflareのオンラインコードエディターでは現状は追加パッケージのインストールができないようなので、上記コードではメールを自力でパースする処理を書いています。

しかも実際に任意のメールを処理するにはこのコードでは全然だめです。エンコードやContent Type毎の対応が必要になりますが、base64のtext/plainしか対応していません。上記のようなコードは、特定のサーバからの通知など送信者が完全に分かっている場合にしか使えないでしょう。

また話は逸れますが、Cloudflareのオンラインコードエディターは現状シンタックスエラーを検知できなかったりデバッグ時のログ出力ができなかったり、本格的な開発に使うには色々と厳しいところがありました。現時点では、あくまでごく簡易的なコードの編集ができる程度のようです。

## 結論
Cloudflare Email WorkersでSlack通知するなら、後述の参考文献のように大人しくWranglerを使って開発環境を構築し[postal-mime](https://github.com/postalsys/postal-mime)などのメールパーサを使うべきだと思います。オンラインコードエディタ上で簡単に作れる！...ような甘い話ではありませんでした。

## 参考文献
- [edevil/email\_worker\_parser: An example of a worker that parses an email](https://github.com/edevil/email_worker_parser)
- [Cloudflare Email Workers でメールを受信したら Chatwork に通知する](https://zenn.dev/takashikakizoe/articles/cloudflare-email-workers-notify-chatwork)