MailCatcherでメールデータのバックアップを手軽に取得する方法

みなさん、開発時のメール送信の検証はどうしていますか？

弊プロダクトでは、MailCatcherというSMTPダミーサーバーを使用しており、メール送信検証は全てMailCatcherを使用しております。

https://mailcatcher.me/

開発環境だけではなく、ディレクターなども触る環境でもメール誤送信防止のため、MailCatcherを使用しているのですが、複数人で触るとたびたび...

「あ、メール間違って削除しちゃいました☆てへぺろ(・ω<)」

といった事故が...

なので、事故っても良いように、MailCatcherのメールデータバックアップを取得するようにしてみました。

## 成果物

https://github.com/yamachita0109/mailcatcher_backup

## 実装する前に...

### ググっても出てこない...

Google先生に聞いてもMailCatcherのバックアップについて何も教えてくれない。。
じゃあ調査するしか

### MailCatcherのデータがどこに保存されているか調査

まず、MailCatcherのデータがどこに保存されているか調査してみた。
物理ファイルで保存されているのであれば、それを定期的にファイルサーバーへPutするだけで良さそうなので。
だが、

```bash
$ find / -type f | xargs grep -n '送信者メールアドレス'
$ 
```

あれ・・？
ないなぁ。じゃあバイナリファイルなのかな。
ソース見るか。

### ソースを見てみる

> The brave can get the source from the GitHub repository.

勇気..?まぁ良い。見てみよう。

https://github.com/sj26/mailcatcher

保存しているところは、[ここら辺](https://github.com/sj26/mailcatcher/blob/main/lib/mail_catcher/mail.rb)だ

```ruby
...
module MailCatcher::Mail extend self
  def db
    @__db ||= begin
      SQLite3::Database.new(":memory:", :type_translation => true).tap do |db|
        db.execute(<<-SQL)
          CREATE TABLE message (
...
```
インメモリに保存していたのか。
別プロセスから接続することはできなかったはず...
仕方がないので、rowデータからバックアップを取ることは諦める。

## 実装!!

### MailCatcherのAPI

MailCatcherにはREST APIが存在する。

> A fairly RESTful URL schema means you can download a list of messages in JSON from /messages, each message's metadata with /messages/:id.json, and then the pertinent parts with /messages/:id.html and /messages/:id.plain for the default HTML and plain text version, /messages/:id/:cid for individual attachments by CID, or the whole message with /messages/:id.source.

試しに...

list.

```bash
$ curl http://localhost:1080/messages
```

```json
[{
  id: 1,
  sender: "<sender@mail.com>",
  recipients: [
    "<recipient@mail.com>"
  ],
  subject: "【subject】mail test.",
  size: "1491",
  created_at: "2023-02-28T04:21:15+00:00"
}]
```

detail.

```bash
$ curl http://localhost:1080/messages/1.plain
```

```json
Hello,

Thank you for reaching us.

On checking , I see that we have already approved your request for the removal of the EC2 email sending limitations on your Amazon Web Services account for the all Regions including Tokyo region ap-northeast-1 !
```

良好。
この二つのAPIを使用すれば、バックアップは取得できそう。

### 構成

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/cfb5928a-f9df-99e6-ff50-e7115c62d86d.png"></img>

| Name | Solution |
|:-:|:-:|
|DeployPipeline|CodePipeline + CloudFormaiotn|
|Runtime|EventBridge + Lambda(node.js@16)|
|Storage|S3|

API経由でMailCatcherからメールデータを取得し、S3に転送。
静的Webホスティングすることで、ブラウザ経由でメールのバックアップを閲覧することができる。

:::note alert
静的Webホスティングする際は、アクセス元を絞ること。
でないと、リリース前のメール内容が他者から閲覧可能になってしまう。
:::

### メインロジック

https://github.com/yamachita0109/mailcatcher_backup/blob/main/controller.js

```js
import fetch from 'node-fetch'
import dt from 'date-utils'
import S3 from './s3.js'

// @see https://mailcatcher.me/
//　Lambdaではなく、ec2等の物理サーバーで稼働させる場合は'http://localhost:1080/messages'とする
const URL = process.env.URL
//　保存先のS3Bucket 静的Webホスティングされてる想定
const BUCKET = process.env.BUCKET
const S3_SITE_URL = `http://${BUCKET}.s3-website-ap-northeast-1.amazonaws.com`

class Controller {
  constructor() {
    this.s3 = new S3(BUCKET)
    // 日次実行想定
    this.date = new Date().toFormat('YYYYMMDD')
  }

  async exec() {
    const response = await fetch(URL)
    // エラーハンドリング？美味しいの？
    const messages = await response.json()
    // 怪しい...が気にしないで
    const summary = [`<h1>${this.date} MailCatcher Backup.</h1>`]

    for await (const message of messages) {
      // htmlメールや、sourceは未対応
      const key = `${message.id}.plain`
      const res = await fetch(`${URL}/${key}`)
      const arrayBuffer = await res.arrayBuffer()
      // メールの文字コード指定
      const text = new TextDecoder('iso-2022-jp').decode(arrayBuffer)

      await this.s3.upload(`${this.date}/${key}`, text)
      // サマリーHTML整形
      summary.push(`<div>subject: ${message.subject}<br />body: <a href="${S3_SITE_URL}/${this.date}/${key}">${this.date}/${key}</a><br />sender: ${message.sender.replace(/[&'`"<>]/g, '')}<br />
          recipients: ${message.recipients.join().replace(/[&'`"<>]/g, '')}<br />created_at: ${message.created_at}<hr></div>`)
    }

    await this.s3.upload(`${this.date}/index.html`, summary.join(''))
  }
}

export default Controller

```

一覧を取得し、詳細分繰り返し、${id}.plainのデータをS3に配置し、サマリー用のHTMLパーツを整形。
処理の最後にサマリー用のHTMLを整形しS3にPut。
※処理の最後にMailCatcher内のメールデータ削除を当初は入れていたが、日跨ぎ検証時に不都合が生じるため、一旦なし。

## 使ってみた!!

一覧は`http://${BUCKET}.s3-website-ap-northeast-1.amazonaws.com/yyyymmdd/index.html`で閲覧可能。
例
http://BUCKET.s3-website-ap-northeast-1.amazonaws.com/20230228/index.html

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/21656503-a54f-96af-5d6f-0bf4bc93b1f0.png"></img>

一覧内のリンクをクリックして、メール本文の閲覧可能。

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/964d68d2-1e3d-320b-cf09-086b74db6850.png"></img>

## 最後に

MailCatcherのソースを修正し、RDSにメールデータをInsertするようにしてみたりしたが、ランニングコストに見合ってないのでボツに。
他にも、APIのエンドポイントを勝手に増やしてみたりして楽しかった、が本筋から逸れ過ぎているのでそれもボツ。

ChatGPTさんはMailCatcherのデータディレクトリ、`~/.mailcatcher`の中にrowデータが存在しているので、ディレクトリ毎バックアップ取れば良いとおっしゃっていたが、`~/.mailcatcher`が見当たらず。

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/31278338-6dd6-afa2-e86c-012f6120c316.png"></img>

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/82c2ac93-2c00-cb7b-5c4a-cc5943f19fd0.png"></img>

私の知らない何かがある様子...
時間があったら解決したいところ。