Raccoon Tech Blog [株式会社ラクーン 技術戦略部ブログ]

株式会社ラクーン 技術戦略部より、tipsやノウハウなど技術的な話題を発信いたします。

Sisimaiによるバウンスメール解析の自動化

こんにちは。管理チームのいとうです。
私は主にインフラの運用管理を担当しています。

今回は弊社でも利用している、"Sisimai" によるバウンスメール解析についてご紹介します。

大規模なメール配信を行っていると、ユーザのメールアドレス登録間違いや、メールアドレス変更の未通知などでバウンスメールが度々発生します。
少数であれば管理者がバウンスメールを確認し、対応を手動で行えばよいかもしれませんが、それが数十~数百アカウントともなれば、到底作業が追いつくはずもありません。
そこで、バウンスメール解析とその後の処理を自動化することで、管理者の負担を減らすことができると考えられます。

本記事では、まず一般的なメールシステムにSisimaiを利用した簡易的なバウンスメール解析アプリケーションを組み込むことで、バウンスメールの情報を扱いやすい形で集約するところまでを目標とします。
また、幾つかのパターンでバウンス解析を行い、結果がどのように出てくるのかについても検証したいと思います。

Sisimaiについて

Sisimaiは株式会社キュービックルート様が開発している、オープンソースのバウンスメール解析用ライブラリです。

製品URL: libsisimai.org

2016年9月8日現在の最新版はv4.18.0です。
mbox形式もしくはMaildir形式のメールを読込み、バウンスメールの内容を解析して出力することができます。
Ruby版とPerl版がリリースされていますが、今回の記事ではRuby版を使用しています。

Ruby版の環境条件

公式サイトにも記載されている通り、Ruby2.1以降もしくはJRuby9.0.4.0以降が必要です。
また、JSON出力用にojが依存ライブラリとしてインストールされます。

余談ですが、以前はbounceHammerという、バウンスメール解析ライブラリと管理用UIがセットになったプロダクトが存在していたのですが、こちらはEOLとなっています(2016年9月現在新規ダウンロードもできないようです)。

Sisimaiの導入

それでは早速Sisimaiを導入していきましょう。
今回は以下の環境で検証を行いました。

検証環境

  • CentOS 6.8
  • Ruby 2.3.1 p112
  • jq (必須ではありませんが、JSON形式の出力を見やすく整形するために利用します。)

  • 導入手順

    jqはCentOSの標準リポジトリには存在しないため、EPELから入手します。
    # yum -y install epel-release
    # yum -y install jq
    

    次にSisimaiを導入します。
    # gem install sisimai
    

    動作確認

    sisimaiの導入が完了したので、早速動作を確認してみましょう。
    sisimaiのgemが導入されたディレクトリ内にset-of-emailsというディレクトリがあります。
    こちらにテスト用に利用できるメールが格納されているので、利用します。
    以下に示すように簡単なワンライナーで、バウンスメールの解析を行うことができます。
    出力される内容は、Sisimaiのデータ構造に詳しく説明されていますので、そちらを参照ください。

    # ruby -r 'sisimai' -e 'puts Sisimai.dump($*.shift)' "set-of-emails/maildir/bsd/x1-01.eml" | jq .
    [
    {
      "timestamp": 1272551685,
      "recipient": "kijitora@example.co.jp",
      "addresser": "shironeko@example.jp",
      "timezoneoffset": "+0900",
      "deliverystatus": "5.0.910",
      "diagnostictype": "",
      "diagnosticcode": "User unknown",
      "subject": "Nyaaaaan",
      "action": "",
      "reason": "filtered",
      "listid": "",
      "alias": "",
      "rhost": "",
      "lhost": "",
      "token": "40c0e0d40e8d1192fb7e4a8e52eee2725fd91671",
      "messageid": "000000000000000000000000000000000000@example.jp",
      "replycode": "",
      "smtpagent": "X1",
      "softbounce": 1,
      "smtpcommand": "",
      "destination": "example.co.jp",
      "senderdomain": "example.jp",
      "feedbacktype": ""
    }
    ]
    

    jqを通すことで、JSONが整形されるのでわかりやすいですね。
    これでSisimaiを利用する環境の準備はOKです。

    バウンスメール解析用プログラムを作る

    それではsisimaiを利用して、バウンスメール解析を自動化するプログラムを作成してみます。
    このシステムでは、解析したデータをデータベースに蓄積していく仕組みを取るようにします。
    今回は、手頃なところでMySQLを利用することにしました。Sequelというgemを利用することで、
    簡単にテーブルにinsertを行えるので、こちらを利用します。
    追加でsequelとmysql2のgemをインストールしておきましょう。
    また、MySQLに以下のような定義のテーブルを持つスキーマを作成しておいてください。

    スキーマ名 sisimai
    テーブル名 bouncemails

    カラム名データ型制約
    idINTPRIMARY KEY, AUTO_INCREMENT
    timestampINT
    recipientVARCHAR(255)
    addresserVARCHAR(255)
    timezone_offsetVARCHAR(32)
    delivery_statusVARCHAR(32)
    diagnostic_typeTEXT
    diagnostic_codeTEXT
    subjectTEXT
    actionVARCHAR(32)
    reasonVARCHAR(32)
    listidVARCHAR(255)
    aliasVARCHAR(255)
    rhostVARCHAR(255)
    lhostVARCHAR(255)
    tokenVARCHAR(255)
    message_idVARCHAR(255)
    reply_codeVARCHAR(32)
    smtp_agentVARCHAR(32)
    softbounceINT
    smtp_commandVARCHAR(32)
    destinationVARCHAR(255)
    sender_domainVARCHAR(32)
    feedback_typeVARCHAR(255)

    次に検証に利用するプログラムを以下に示します。検証用のため、エラー処理などは省いています。

    バウンスメール取り込み用プログラム

    メールボックスのあるサーバに解析プログラムを配置し、直接メールを取り込んでも良いのですが、今回はリモートのサーバからPOP3でアクセスし、メールを取得することにしました。
    以下のプログラムでバウンスメール解析用サーバの適当なディレクトリにメールファイルをダウンロードしてきます。

    bounce-receiver.rb
    # coding: utf-8
    require 'net/pop'
    
    basedir = '/var/apps/bounce-parser'
    mailstore = basedir + '/mailstore'
    
    host = 'mail.example.com'
    port = 110
    user = 'bounce'
    password = 'bounce'
    
    # Connect to pop3 server
    pop = Net::POP3.new(host, port)
    pop.start(user, password)
    
    # Receive mails
    unless pop.mails.empty?
      pop.mails.each do |mail|
        filename = "#{mailstore}/" + Time.now.strftime('%Y%m%d%H%M%S%L') + ".eml"
        fd = File.open(filename, 'w')
        fd.write mail.pop
        mail.delete
        sleep 1
      end
    end
    
    pop.finish
    

    次に、Sisimaiでメールの解析を行うプログラムのサンプルです。
    先ほどダウンロードしたメールのディレクトリを指定することで、ディレクトリ内に存在するメールを一括で解析し、結果をSisimai::Dataオブジェクトに変換してくれます。
    このオブジェクトに対しdamnメソッドを実行すると、hash形式で結果を取り出すことができます。
    取り出したデータはsequelのinsertメソッドを利用し、データベースに投入されます。

    bounce-parser.rb
    # coding: utf-8
    require 'sisimai'
    require 'sequel'
    require 'mysql2'
    
    basedir = '/var/apps/bounce-parser'
    mailstore = basedir + '/mailstore'
    database = 'bounce'
    host = 'localhost'
    port = 3306
    user = 'bounce'
    password = 'bounce'
    encoding = 'utf8'
    
    parsed_records = []
    
    bounce_mails = Sisimai.make(mailstore)
    
    unless bounce_mails.nil?
      bounce_mails.each do |bounce_mail|
        parsed_records << bounce_mail.damn
      end
    else
      puts "mail not found"
      exit(0)
    end
    
    DB = Sequel.mysql2(
           :database => database,
           :host     => host,
           :port     => port,
           :user     => user,
           :password => password,
           :encoding => encoding
         )
    
    unless parsed_records.nil?
      parsed_records.each do |record|
        DB[:mail_logs].insert(
          :timestamp       => record['timestamp'],
          :recipient       => record['recipient'],
          :addresser       => record['addresser'],
          :timezone_offset => record['timezoneoffset'],
          :delivery_status => record['deliverystatus'],
          :diagnostic_type => record['diagnostictype'],
          :diagnostic_code => record['diagnosticcode'],
          :subject         => record['subject'],
          :action          => record['action'],
          :reason          => record['reason'],
          :listid          => record['listid'],
          :alias           => record['alias'],
          :rhost           => record['rhost'],
          :lhost           => record['lhost'],
          :token           => record['token'],
          :message_id      => record['messageid'],
          :reply_code      => record['replycode'],
          :smtp_agent      => record['smtpagent'],
          :softbounce      => record['softbounce'],
          :smtp_command    => record['smtpcommand'],
          :destination     => record['destination'],
          :sender_domain   => record['senderdomain'],
          :feedback_type   => record['feedbacktype']
        )
      end
    end
    

    これで結果の解析からデータの集約まで行うことができました。
    2つの簡易なプログラムで一通りの作業を行えるので楽ですね。
    次のセクションでは、実際にメールシステムに組み込んで検証を行ってみたいと思います。

    メールシステムに組み込んで検証してみる


    よくあるシステム構成パターンに組み込んで動作を確認してみたいと思います。
    今回は検証用に以下のようなメールシステムを構築してみました。
    LAN内にクローズなメール環境を構築したため、他にもアクターは存在しますが、単純化するため一部を省略しています。
    overview
     
    次にサーバの役割をドメイン毎に軽く説明します。

    example.comのメールシステム

    こちらがメール送信元のメールシステムになります。
    メールゲートウェイサーバがメールボックスも受け持つ、シンプルな構成です。
    また、バウンスメール解析用のプログラムを配置したサーバを用意しています。

    メールゲートウェイサーバ
  • postfix
  • dovecot

  • バウンスメール解析サーバ
  • bounce-receiver.rb (pop受信プログラム)
  • bounce-parser.rb (解析プログラム)

  • example.co.jpのメールシステム

    こちらが送信されたメールの宛先となるシステムです。
    外部との接点になるゲートウェイサーバと配信用サーバは別に分かれているという想定です。

    メールゲートウェイサーバ
  • postfix
  • メール配信サーバ
  • postfix
  • dovecot

  • 正常にメールが配信される場合

    まずはメールが正常に配信される場合を見てみましょう。
    delivered

    1. example.comのGWサーバはexample.co.jpのGWサーバにメールをリレーします。
    2. メールを受け取ったexample.co.jpのGWサーバは、自ドメイン宛のメールなので、メール配信サーバへとリレーを行います。
    3. メール配信サーバはGWサーバからのSMTP接続を受け付け、対象のアカウントが存在することを確認し、メールをローカルのメールボックスへ配送します。

    メールがバウンスとなる場合

    次にメールがバウンスとなる場合を見てみましょう。
    メールがバウンスになるパターンは様々ですが、ここでは宛先アカウントが存在しない場合のバウンスメールの流れを例に取ります。
    bounce

    1. example.comのGWサーバはexample.co.jpのGWサーバにメールをリレーする。
    2. メールを受け取ったexample.co.jpのGWサーバは、自ドメイン宛のメールなので、メール配信サーバへリレーしようとする。
    3. 配信サーバはexample.co.jpのGWサーバのSMTP接続を受け付けるが、対象のアカウントが存在しないので、エラーを通知する。
    4. example.co.jpのGWサーバは元メールのEnvelope From宛にバウンスメールを送信する。
    5. example.comのGWサーバはメールを受け取り、対象アカウントのメールボックスにストアする。
    6. バウンスメール解析サーバは、定期的にバウンスメール受信用アカウントのメールボックスをチェックし、取り込みと解析を行う。
    流れがやや複雑ですが、このようにして送信したメールがバウンスメールとなって戻ってきます。

    パターン別でバウンス解析結果を検証する


    今回は以下の3パターンで返されたバウンスメールに対し、Sisimaiの解析結果がどう変化するかを検証してみます。

    パターン1: ハードバウンス
    宛先となるアカウントが存在しなかったり宛先のドメインがないなど、
    恒久的な理由でメール配送されず戻ってきたパターンです。

    パターン2: ソフトバウンス
    宛先となるアカウントは存在するがメールボックスの容量が超過しているため受信できないなど、
    一時的な理由でメールが配送できず戻ってきたパターンです。

    パターン3: ML配信におけるバウンス
    1対1のパターンだけでなく、メーリングリストにメールを送信するパターンも確認してみます。
    今回はML内の一部のアカウントが存在しないという想定で試してみます。

    1. ハードバウンス

    ますはじめにハードバウンスのパターンを確認してみます。
    以下のように、存在しない宛先に向けてメールを送信してみます。

  • 送信元: test@example.com
  • 宛先: john@example.co.jp(実際には存在しない宛先)

  • このパターンでは宛先john@example.co.jpが存在しないので、ハードバウンスとなってメールが返ってくると想定されます。
    では、解析プログラムを通した結果をDBから参照してみましょう。

    pattern1

    結果のカラム数が多いため5行に分割していますがご了承ください。
    バウンスメールに関して、かなり詳細な情報が取得できているようですね。
    この結果を元に、バウンスメールの内容をざっくりと解析してみたいと思います。
    1. addresserとrecipientの内容から、test@example.comが送信元でjohn@example.co.jpが宛先だということがわかります。
    2. reply_codeおよびdelivery_statusが550/5.1.1ということから、「宛先ユーザアカウントが正しくないためメール送信に失敗した」ということがわかります。
    3. softbounceの値は0(ハードバウンス), 1(ソフトバウンス), -1(不明)の3値を取りますが、ここでは"0"となっているので、ハードバウンスであることがわかります。
    4. rhost, lhostに着目するとバウンス発生時にSMTP通信を行っていたリモートホストとローカルホストが分かるので、メールの経路上どこでバウンスになってしまったかがわかります。
    確かに宛先ユーザが存在しないため、ハードバウンスになって返ってきていることがわかりますね。

    2. ソフトバウンス

    次にソフトバウンスのパターンを検証してみます。
    以下に示す宛先に対し、メール配信サーバの受信可能サイズを制限して、メールを送信してみます。

  • 送信元: test@example.com
  • 宛先: paul@example.co.jp

  • このパターンでは、宛先paul@example.co.jpは存在しますが、メール配信サーバの受信サイズ制限に引っかかってしまいます。
    ただ、メールサイズを小さくすれば正常に配信が可能ですので、ソフトバウンスとして返ってくることが想定されます。
    では、解析プログラムの結果を確認しましょう。

    pattern2

    1. addresserとrecipientの内容から、test@example.comが送信元でjohn@example.co.jpが宛先だということがわかります。
    2. reply_codeはNULL(値が取得できなかった)ですが、delivery_statusが5.3.4でありdiagnostic_codeの'message size xxxx exceeds size limit xxx of server ...'の記述から、宛先メールサーバのサイズ制限に引っかかっていることがわかります。
    3. softbounceの値が"1"となっているので、ソフトバウンスであることがわかります。

    3. ML配信におけるバウンス

    最後にメーリングリストにあててメールを送信したパターンを確認します。

  • 送信元: test@example.com
  • 宛先: ml@example.co.jp (john@example.co.jp, paul@example.co.jpのエイリアス)

  • このパターンではml@example.co.jpはjohnとpaulのアカウントのaliasとなっています。
    しかしながらjohnのアカウントは存在しないため、johnに送信されたメールはハードバウンスとして返ってきます。
    次に解析プログラムの結果を確認します。

    pattern3

    パターン1の場合と似た内容ですが、recipientはjohn@example.co.jpに、aliasはml@example.co.jpになっています。
    この内容から、エイリアスから転送を受けたアカウントが存在しないためバウンスメールが発生していることがわかります。
    また、softbounceが"0"であることから、ハードバウンスであることがわかります。

    まとめ

    今回はSisimaiによるバウンスメール解析について紹介しました。
    実際のメールシステムを模した構成で、3パターンでバウンスメール解析の検証を行ってみましたが、いかがでしたか?
    ハードバウンス・ソフトバウンスの判定ができたり、ML所属アカウントからのバウンスもいい感じに解析できるので、
    バウンスメール処理の自動化に活用できるのではないかと思います。
    ユースケースとしては、

  • 定期バッチで特定アカウントからのバウンスメールの数が閾値を超えたら、メール配信を停止する
  • ユーザのメールアドレス登録間違いを検知し、管理者に通知する
  • 日々バウンスデータを取り込み、送達率向上の施策を取るための材料にする

  • など、様々な使いみちが考えられます。
    導入も簡単なので、ぜひ一度試してみてはいかがでしょうか?

    自動化やスクレイピングに使えるやつ XPath

    開発チームの下田です。

    XMLの特定の部分を指定するためのXML Path Language(以下、XPathと表記します)という言語があります。
    HTMLを解析して自動テストの作成やスクレイピング、XML設定ファイルの書き換えに使います。簡単な割に、覚えておくと地味に便利なやつです。

    とりあえず書いてみる

    何はともあれ、覚えるためには書いてみます。

    ブラウザが一番手軽なXPathの実行環境だと思います。FirefoxとChromeはXPathが実装されています。コンソールで次のコードを実行してみてください。コンソールはF12を押せば開きます。

    document.evaluate('//html/head/title', document, null, XPathResult.STRING_TYPE, null).stringValue

    正常に実行できれば、開いているページのタイトルが表示されます。
    続きを読む

    AWS LambdaとAPI Gatewayを利用し、PagerDutyのインシデント発生時にSlackに専用チャンネルを作成する #3

    こんにちは、インフラ及びシステム運用を担当している田中です。

    前回の記事ではPagerDutyのWebhookをLambda関数で受け、SlackのIncident専用のチャンネルを作成してメッセージをpostしたり、IncidentがResolvedになったら専用チャンネルをarchiveしたりするところまでご紹介しました。

    大まかにまとめると、下記のシーケンス図のような仕組みになります。

    sequence_1

    しかし、専用チャンネルまで作成したのにpostしたメッセージは簡単なテキストだけで、見栄えもぱっとしなければ、大した情報も含まれていませんでした。

    そこで今回は、Slackにpostするメッセージの見栄えを改善したり、障害対応に必要な情報をメッセージに含めるなど、前回作ったプログラムに機能を追加していきます。

    また、せっかくLambdaを使用しているのに他のAWSの機能を使わないのはもったいないので、Node.jsのAWS SDKを利用してAWSの機能を利用する方法についても説明していきたいと思います。
    続きを読む
    記事検索