Sisimaiを使ったバウンスメールの管理

最近、Ninja650に乗り換えたSREチームの菅原です。今までマルチばかり乗ってきたんですが、ツインもなかなか面白いですね。シフトペダルをガチャコンいわせながら方々に出かける毎日です。

この記事では、サービスが配信しようとして何らかの理由で差し戻されたメール—バウンスメールの管理をどのように行っているかという話しを書きます。

バウンスメール

サービスがユーザに向けてメールを配信しようとすると、多かれ少なかれバウンスメールは発生します。メールアドレスが間違っている・携帯電話の設定で受信を拒否している・メールボックスが一杯にになっている・IPアドレスがブラックリストに載ってしまったためサーバにメールの受信を拒否されている…etc。完全になくすことは難しいですが、バウンスメールを放置するとメールの到達率を下げたり、送信先から一時的にメールの受信を拒否されたりすることがあるため、差し戻されてしまった宛先はリストに登録して、再送を抑止することが望ましいです。

SendGridのようなサービスを利用している場合、差し戻されたメールは自動的にリストに登録されて再度メールを送っても配送されないようになっていたりするのですが、クックパッドの場合は内部システムのPostfixサーバからメールを配信していたため、バウンスメールの管理をある程度、自前で作り込む必要がありました。

既存のシステム

以前はbounceHammerというOSSとMySQLのバウンスメール管理用のテーブルを使って、バウンスメールの管理が行われていました。

f:id:winebarrel:20170502181740p:plain

  1. アプリケーションはメール送信サーバのPostfixを経由して、メールを配信します
  2. 差し戻されたメールは、バウンスメール管理サーバのPostfixが受け取ります
  3. Postfixが受け取ったメールをbounceHammerが解析して自身のデータベースに入れます
  4. 定期実行されるスクリプトがbounceHammerのデータベースからアプリ用のデータベースにバウンスメール情報をマイグレーションします
  5. アプリケーションはユーザテーブルとバウンスリストを結合して、メールが差し戻された宛先にはメールを送らないようにします
  6. バウンスリストの情報は、管理用アプリケーションから削除することができます

既存システムの問題点

このバウンスメール管理システムには、いくつか問題点がありました。

  • bounceHammerの導入が後付けであったため、バウンスメール情報がbounceHammerとアプリ用データベースで二重管理になっていた
  • 同様に後付けが原因で、管理アプリケーションからアプリ用データベースの情報は削除できるが、bounceHammerの情報を削除(ホワイトリスト登録)できないため、手作業で同期を取る必要があった
  • ユーザテーブルとバウンスリストをSQLで結合して配信対象をフィルタリングする方式であったため、スケーラビリティに問題があった
  • バウンスリストがテーブルという単位で管理されているため、アプリケーションの各機能が個別に配信対象のフィルタリングを実装する必要があった
  • SQLの結合という形でフィルタリングを行うと、メールが配信されなかったユーザがそもそも配信の対象にならなかったのか、バウンスリストに登録されていたため配信されなかったのか、区別を行うことが難しかった
  • バウンスメールの情報がきちんと可視化されていなかったため、配信状況の把握に難があった
  • bounceHammerがEOLになった

問題点を抱えた状態での運用がつらく、またbounceHammerがEOLになったこと、さらにシステムのリニューアル作業が進行中だったこともあり、バウンスメール管理システムを新しく作り直すことにしました。そして、そのコアの部分として利用することになったのがSisimaiです。

Sisimai

SisimaiはbounceHammerの後継として開発されているバウンスメール解析ライブラリです。bounceHammerが管理用Webアプリや集計用のスクリプトも含めた複合的なシステムであったのに対して、シンプルなPerl・Rubyのライブラリです。ライブラリの依存関係も少なく、またわかりやすいAPIで、しかも自分が慣れたRubyのライブラリであったため、とても簡単に新しいシステムに組み込むことができました。

以下はSisimaiを使ってバウンスメールの解析を行うサンプルコードです(※公式ドキュメントより引用)

#! /usr/bin/env ruby
require 'sisimai'
v = Sisimai.make('/path/to/mbox') # or Path to Maildir

if v.is_a? Array
  v.each do |e|
    puts e.addresser.address    # shironeko@example.org # From
    puts e.recipient.address    # kijitora@example.jp   # To
    puts e.recipient.host       # example.jp
    puts e.deliverystatus       # 5.1.1
    puts e.replycode            # 550
    puts e.reason               # userunknown
  end
else
  # There is no bounce message in the mailbox
  # or Sisimai could not parse
end

Sisito

Sisimaiはすばらしいライブラリなのですが、bounceHammerにあったような管理用のWebアプリケーションはなくなってしまいました。エンジニア以外のスタッフが「問い合わせのあったメールアドレスをホワイトリストに登録する」等の作業が発生するため、管理用のWebアプリケーションは必須です。そこで以前の運用経験を踏まえ、バウンスメール情報保存用のデータベースとそれを管理するウェブアプリケーションを作成しました。それがSisitoです。

以下はSisitoの管理画面のスクリーンショットです。

f:id:winebarrel:20170508161014p:plain f:id:winebarrel:20170508161019p:plain

また、バウンスメール管理システム以外からバウンスメールの情報を取得するため、sisito-apiというAPIサーバも作成しました。

新バウンスメール管理システム

SisimaiとSisitoを使った新しいバウンスメール管理システムの構成が以下のようになります。

f:id:winebarrel:20170508153346p:plain

  1. アプリケーションはメール配信サーバのPostfixを経由してメールを配信します。このときPostfixの機能でblacklistに登録されているメールアドレスには配信しないようにします
  2. 差し戻されたメールはメール配信サーバに保存されます
  3. 定期実行されるSisimaiスクリプトがSisitoデータベースにバウンスメール情報をを保存します
  4. 定期実行されるblacklist更新スクリプトが一定の条件に従って(ハードバウンスのみ ・特定の理由のみ、など)blacklistを更新します
  5. Sisitoを使って統計情報の閲覧やホワイトリストへの登録を行います
  6. アプリケーションはsisito-apiを経由してバウンスメールの情報を取得します
  7. メールの送信ステータス・Subject・blacklistによるリジェクト状況などのログはElasticsearchに送信してKibanaで閲覧できるようにします

まとめ

新バウンスメール管理システムの導入により、アプリケーションの各機能で配信制御を行う必要がなくなり、Postfixでフィルタリングを行うことでスケーラビリティの問題も解決することができました。また、Sisitoによる可視化により問題が発生しても(たとえば、特定の理由のバウンスメールが増えているなど)状況をすぐに把握して迅速に対応することができるようになりました。さらにホワイトリストの登録処理がエンジニアを介さずにできるようになったため、業務フローのコストも下げることができました。

差し戻されるメールのエラー内容はサービスによって様々なパターンがあり、人間が解析することはかなりの労力を伴います。SisimaiのようなライブラリがOSSとして公開されていることは大変ありがたいことです。積極的に活用して、フィードバックなどで開発に貢献していきたいと考えています。

バウンスメールの解析で苦労しているかたは、一度Sisimaiを試してみてはどうでしょうか?

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/