Cloudflare Email Workers でメールの添付ファイルをR2へ保存する
はじめに
この記事はCloudflare Email Workers に関する記事です。
- 受信メールの添付ファイルを別途保管しておきたい
- Email Workers から R2 を利用したい
上記に当てはまる方のお役に立てるかと思います。
参照
Cloudflare R2
Cloudflare R2は、Amazon S3と互換性のあるクラウド型のオブジェクトストレージサービスで、
Cloudflare Workersと完全に統合されたサーバーレスランタイムです。
ストレージバケットに書き込まれたり、読み取られたりするオブジェクトを動的に変換することを可能にします。
エグレス料金ゼロ、グローバルに分散されたオブジェクトストレージです、素敵です。
無料枠
期限のない無料枠があるので試しやすいかと思います。
ストレージ: 10GB/月
クラスA操作(状態を変更する): 100万/月
クラスB操作(既存の状態を読み取る): 1,000万/月
Workers プロジェクトを作成
コマンドにて作成
以下のコマンドで作成しました
npm create cloudflare@2 -- email-workers-save-attachment-r2
後ほど全て置き換えるので "Hello World" Worker
を選択し、今回は TypeScript Yes
を選択しました。
using create-cloudflare version 2.6.1
╭ Create an application with Cloudflare Step 1 of 3
│
├ In which directory do you want to create your application?
│ dir ./email-workers-save-attachment-r2
│
├ What type of application do you want to create?
│ type "Hello World" Worker
│
├ Do you want to use TypeScript?
│ yes typescript
│
├ Copying files from "hello-world" template
│
├ Retrieving current workerd compatibility date
│ compatibility date 2023-10-16
│
├ Do you want to use git for version control?
│ yes git
│
╰ Application created
╭ Installing dependencies Step 2 of 3
│
├ Installing dependencies
│ installed via `npm install`
│
├ Committing new files
│ git commit
│
╰ Dependencies Installed
╭ Deploy with Cloudflare Step 3 of 3
│
├ Do you want to deploy your application?
│ no deploy via `npm run deploy`
│
├ APPLICATION CREATED Deploy your application with npm run deploy
│
│ Navigate to the new directory cd email-workers-save-attachment-r2
│ Run the development server npm run start
│ Deploy your application npm run deploy
│ Read the documentation https://developers.cloudflare.com/workers
│ Stuck? Join us at https://discord.gg/cloudflaredev
│
╰ See you again soon!
プロジェクトディレクトリへ移動
cd email-workers-save-attachment-r2
postal-mime の追加
npm install postal-mime
R2のバケットを作成しておく
wrangler r2 bucket create mail-attachment
⛅️ wrangler 3.14.0
-------------------
Creating bucket mail-attachment.
Created bucket mail-attachment.
wrangler.toml の編集
環境変数を設定するために wrangler.toml
を編集します
以下の情報へ変更して下さい
今回はMAIL_WORKER_BUCKET
という名前で作成したバケットを指定しています
-
CHATWORK_TOKEN
は Chatwork API のトークン -
CHATWORK_ROOM
は Chatwork のルームID -
RELAY_EMAILS
は 転送先のメールアドレスの配列 -
MAIL_WORKER_BUCKET
は R2のバケット名
name = "email-workers-save-attachment-r2"
main = "src/index.ts"
compatibility_date = "2023-10-16"
node_compat = true
workers_dev = true
vars = { CHATWORK_TOKEN = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", CHATWORK_ROOM = "XXXXXXXXX", RELAY_EMAILS = '["email1@example.com", "email2@example.com"]' }
[[r2_buckets]]
binding = "MAIL_WORKER_BUCKET"
bucket_name = "mail-attachment"
index.ts の編集
前回からの主な変更点
前回の index.js から index.ts へ変更し、添付ファイルへの対応を追加しました。
parsedEmailのattachmentを受け取り、await env.MAIL_WORKER_BUCKET.put()
にてR2のバケットに保存しています。
const processAttachments = async (attachments: any[], env: Environment, messageId: string): Promise<string> => {
if (attachments.length === 0) {
console.log('No attachments');
return 'No attachments';
}
let attachmentInfo = '';
for (const att of attachments) {
console.log('Attachment: ', att.filename);
console.log('Attachment disposition: ', att.disposition);
console.log('Attachment mime type: ', att.mimeType);
console.log('Attachment content: ', att.content);
let attachmentKey = `${messageId}_${att.filename}`;
await env.MAIL_WORKER_BUCKET.put(attachmentKey, att.content);
attachmentInfo += attachmentKey + '\n';
}
return attachmentInfo;
};
index.ts の全文
以下の機能を持つように実装しております。
- メール受信をトリガーに実行
- 添付ファイルを R2 へ保存
- 通知文を作成し Chatwork へ通知
- 転送先のメールアドレスへ転送
import PostalMime from 'postal-mime';
const CW_API_ENDPOINT = 'https://api.chatwork.com/v2/rooms/';
interface Environment {
CHATWORK_TOKEN: string;
CHATWORK_ROOM: string;
RELAY_EMAILS: string;
MAIL_WORKER_BUCKET: R2Bucket;
}
interface MessageContext {
waitUntil: (promise: Promise<void>) => void;
}
interface Message {
from: string;
to: string;
headers: Map<string, string>;
raw: ReadableStream<Uint8Array>;
rawSize: number;
forward: (email: string) => Promise<void>;
}
export default {
async email(message: Message, env: Environment, ctx: MessageContext): Promise<void> {
ctx.waitUntil(notifyMessage(message, env));
}
};
const streamToArrayBuffer = async (stream: ReadableStream<Uint8Array>, streamSize: number): Promise<Uint8Array> => {
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;
};
const processAttachments = async (attachments: any[], env: Environment, messageId: string): Promise<string> => {
if (attachments.length === 0) {
console.log('No attachments');
return 'No attachments';
}
let attachmentInfo = '';
for (const att of attachments) {
console.log('Attachment: ', att.filename);
console.log('Attachment disposition: ', att.disposition);
console.log('Attachment mime type: ', att.mimeType);
console.log('Attachment content: ', att.content);
let attachmentKey = `${messageId}_${att.filename}`;
await env.MAIL_WORKER_BUCKET.put(attachmentKey, att.content);
attachmentInfo += attachmentKey + '\n';
}
return attachmentInfo;
};
const buildNotifyMessage = async (
message: Message,
parsedEmail: any,
relayEmails: string[],
env: Environment
): Promise<string> => {
let messageId = message.headers.get('message-id');
messageId = messageId ? messageId.replace(/^<|>$/g, '') : '';
const attachments = await processAttachments(parsedEmail.attachments, env, messageId);
let emailText = parsedEmail.text;
if (emailText.length > 600) {
emailText = emailText.slice(0, 600) + '...';
}
return `
[info]
[title]Notifications from Cloudflare Mail Email Workers[/title]
From: ${message.from}
To: ${message.to}
[info]
Title: ${message.headers.get('subject')}
Body:
${emailText}
[/info]
[code]
Attachment:
${attachments}
[/code]
[code]
Message-id: ${message.headers.get('message-id')}
Received: ${message.headers.get('received')}
Date: ${message.headers.get('date')}
[/code]
[code]
# Forwarding Email Addresses
${relayEmails.join(', ')}
[/code]
[/info]`;
};
const sendChatwork = async (notifyMessage: string, env: Environment): Promise<void> => {
try {
const cwBody = new URLSearchParams({
body: notifyMessage
});
const cwHeaders = new Headers();
cwHeaders.append('Content-Type', 'application/x-www-form-urlencoded');
cwHeaders.append('X-ChatWorkToken', env.CHATWORK_TOKEN);
const cwRequest = new Request(`${CW_API_ENDPOINT}${env.CHATWORK_ROOM}/messages/?${cwBody.toString()}`, {
headers: cwHeaders,
method: 'POST'
});
let cwResponse = await fetch(cwRequest);
console.log(cwResponse);
} catch (e) {
console.error(e);
}
};
const notifyMessage = async function(message: Message, env: Environment): Promise<void> {
try {
const relayEmails: string[] = JSON.parse(env.RELAY_EMAILS);
const rawEmail = await streamToArrayBuffer(message.raw, message.rawSize);
const parser = new PostalMime();
const parsedEmail = await parser.parse(rawEmail);
const notifyMessage = await buildNotifyMessage(message, parsedEmail, relayEmails, env);
console.log('parsedEmail: ', parsedEmail);
await Promise.all(relayEmails.map(email => message.forward(email)));
await sendChatwork(notifyMessage, env);
} catch (e) {
console.error(e);
}
};
Cloudflare Workers へデプロイ
以下のコマンドでログイン中のアカウントへデプロイが可能です
wrangler deploy
メール本文のPDF化は?
業務でも非常によく利用しているPuppeteerが使えると思ったのですが、
waitlist に登録して待ちが必要のようです。
(登録したので楽しみに待っております!)
その他 ライブラリは重さなどの制約に引っかかりそうなので少しトライしてやめておきました。
Lambda での PDF はこちらから
最後に
前回に引き続き Cloudflare Email Workers のお話でした。
これでメールを受信したら R2へ添付ファイルを保存することができます。
Cloudflare D1 等も利用して電子請求書への対応などができたら良いなと思っています。
最後まで読んでいただき、ありがとうございました!
Discussion