1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Sisitoとsisimaiでバウンスメールの解析、集計する

Last updated at Posted at 2022-11-24

はじめに

さくらインターネットのさくらのVPSを利用し、Postfix + mailmanで登録者数2,000名規模のメーリングリストを運用しています。
いままではbounceHammerを利用してバウンスメールの解析と集計を行っていました。しかし、2016年にEOLとなったためバウンスメールの解析をbounceHammerの後継であるSisimaiで行うことにしました。Sisimaiには管理用のWebアプリケーションが用意されていないため、クックパッドが公開しているsisitoを利用しました。

参考情報

ゴール

Sisimaiでバウンスメールを解析し、sisitoで集計を行えるようにします。

sisito Web管理画面

バウンスメール解析、集計の流れ

  1. メール配信サーバーから受信者宛にメールが送信されます。
  2. バウンスしたメールはメール配信サーバーに戻されます。
  3. メール配信サーバーで運用しているmailmanの設定により、バウンスメール受信用のgmailアドレスに転送されます。
  4. Sisimai, sisitoを運用しているサーバーでgmailのメールボックスからバウンスメールをPOP3Sで取得します。
  5. Sisimaiでバウンスメールを解析し、sisitoで集計を行います。

gmailを経由せず直接POP3でバウンスメールを取得する方法もありますが、POP3サーバーの構築と運用が手間だったのでgmailに転送しています。

バウンスメール解析、集計の流れ

環境構築

前提条件

以下のOS、ミドルウェアを使用します。

  • Ubuntu 22.04.1 LTS (GNU/Linux 5.15.0-52-generic x86_64)
  • ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]
  • Rails 7.0.4
  • mysql Ver 8.0.31-0ubuntu0.22.04.1 for Linux on x86_64 ((Ubuntu))

OSのバージョン

OSはUbuntu 22.04.1 LTを使用します。

$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.1 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.1 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

Rubyや必要なライブラリなどのインストール

OSを最新の状態にし、必要なライブラリをインストールします。

$ sudo apt-get update && 
  sudo apt-get upgrade && 
  sudo apt-get install -y --no-install-recommends \
      build-essential \
      libxml2-dev \
      libxslt-dev \
      mysql-client \
      mysql-server \
      libmysqlclient-dev \
      nodejs \
      libsqlite3-dev \
      liblzma-dev \
      zlib1g-dev \
      libssl-dev \
      libreadline-dev \
      libyaml-dev \
      libxslt1-dev \
      software-properties-common \
      libffi-dev \
      tzdata \
      jq && \
  sudo apt-get clean && \
  sudo rm -rf /var/lib/apt/lists/*

必要に応じてタイムゾーンの設定を行います。

$ sudo cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
$ sudo dpkg-reconfigure -f noninteractive tzdata

rbenvでRubyをインストールします。

$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc
$ exec $SHELL
$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
$ echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc
$ exec $SHELL

Ruby 3.1.2をインストールします。

$ rbenv install 3.1.2
$ rbenv global 3.1.2
$ gem install bundler

Sisimai, sisitoのインストール

sisitoのGitリポジトリにあるREADME.mdを参考にインストールを行います。ここでは、本家のリポジトリをforkしRubyやRailsのバージョンを上げたものを使用します。
本家のリポジトリにあるsisitoはすでにEOLとなっているRuby 2.3.0を前提としていることや、定義されているDBスキーマでは一部バウンスメールのエラーメッセージが格納できないため、それらを修正しました。
SisimaiはGemfileで定義されているため、個別のインストールは不要です。

$ git clone https://github.com/revsystem/sisito.git -b Rails_v7.0.4
$ cd ./sisito
$ bundle update --bundler
$ bundle install

MySQLの準備

MySQLを有効化し起動します。

$ sudo systemctl enable mysql
$ sudo systemctl start mysql

cloneしたディレクトリの config/database.yml に3つのDB接続情報があります。ここでは、専用の bounce ユーザーを作成し、それを config/database.yml に設定します。
以下は、ユーザー名を bounce 、パスワードを user_password_1234 とした例です。

 CREATE USER 'bounce'@'localhost' IDENTIFIED BY 'user_password_1234';
 GRANT ALL PRIVILEGES ON sisito_development.* TO 'bounce'@'localhost' WITH GRANT OPTION;
 GRANT ALL PRIVILEGES ON sisito_test.* TO 'bounce'@'localhost' WITH GRANT OPTION;
 GRANT ALL PRIVILEGES ON sisito_production.* TO 'bounce'@'localhost' WITH GRANT OPTION;

sisito_test, sisito_productionも同様にusername,passwordを作成したもので書き換えます。

config/database.yml
development:
  adapter: mysql2
  encoding: utf8
  reconnect: false
  database: sisito_development
  pool: 5
  username: bounce
  password: user_password_1234
  host: localhost
  variables:
    sql_mode: TRADITIONAL # required

sisitoのDBスキーマに従い、DBを作成します。

$ bundle exec rails db:create
$ bundle exec rails db:migrate

Sisimaiでバウンスメールを解析してみる

Sisimaiのgemがインストールされたディレクトリ以下にバウンスメールのサンプルが格納された set-of-emails というディレクトリがあります。このサンプルのひとつを使ってバウンスメールを解析してみます。
Gemがインストールされているディレクトリを探します。

$ gem environment gemdir
/home/user1/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0

サンプルバウンスメールを解析します。

$ ruby -r 'sisimai' -e 'puts Sisimai.dump($*.shift)' "/home/user1/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/sisimai-4.25.14/set-of-emails/maildir/bsd/lhost-gsuite-01.eml" | jq .

以下のような結果が得られます。解析結果のデータ構造はSisimai::Dataのデータ構造にドキュメントが用意されています。
この解析結果からバウンスの理由はuserunknown、相手先のメールシステムはGSuite などといったことが分かります。sisitoはこのような解析結果を集計し、Web管理画面に表示します。

[
  {
    "catch": "",
    "token": "b5f9067b3aafc728880f7352d85f21a563974b71",
    "lhost": "",
    "rhost": "192.0.2.222",
    "alias": "",
    "listid": "",
    "reason": "userunknown",
    "action": "failed",
    "origin": "/home/user1/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/sisimai-4.25.14/set-of-emails/maildir/bsd/lhost-gsuite-01.eml",
    "subject": "Nyaaan",
    "messageid": "00222222-2222-2222-2222-FF00FFFF0000@example.jp",
    "replycode": "550",
    "smtpagent": "GSuite",
    "softbounce": 0,
    "smtpcommand": "",
    "destination": "example.de",
    "senderdomain": "example.jp",
    "feedbacktype": "",
    "diagnosticcode": "** Address not found ** Your message wasn't delivered to kijitora@example.de because the address couldn't be found. Check for typos or unnecessary spaces and try again.The response from the remote server was: 550 #5.1.0 Address rejected.",
    "diagnostictype": "SMTP",
    "deliverystatus": "5.1.0",
    "timezoneoffset": "-0700",
    "addresser": "sironeko@example.jp",
    "recipient": "kijitora@example.de",
    "timestamp": 1490384096
  }
]

Gmailのメールボックスからメールを取得する。

あらかじめ用意したgmailアドレスのメールボックスにバウンスメールを転送しているので、Sisimai,sisitoを運用しているサーバーからPOP3Sで接続しバウンスメールを取得します。

メールを取得するスクリプトを配置したディレクトリをスクリプト内の basedir に設定します。
userにはgmailアドレスを設定し、passwordにはgmailアカウントのパスワードではなくアプリパスワードを設定します。
アプリパスワードアプリ パスワードを作成、使用するにある手順で作成できます。

gmailの設定画面でPOPを有効にします。(すべてのメールでPOPを有効にするもしくは、今後受信するメールでPOPを有効にするを選択)
gmail_pop.png

bounce-receiver.rb
# config: utf-8
require 'net/pop'
basedir = '/home/user1/sisito'
mailstore = basedir + '/mailstore'
host = 'pop.gmail.com'
port = 995
user = 'YOUR-ACCOUNT@gmail.com'
password = 'アプリパスワード'
# Connect to pop3 server
Net::POP3.enable_ssl(OpenSSL::SSL::VERIFY_NONE)
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

上記で設定したbasedir以下にメールを格納するディレクトリを作成します。

$ mkdir ./mailstore

スクリプトを実行しメールを受信します。gmailのPOP3Sは一度のアクセスで250通しか受信できないため、バウンスメールが250通以上ある場合はこのスクリプトを複数回実行する必要があります。

$ ruby bounce-receiver.rb

取得したメールはこのように保存されます。

$ ls -l mailstore
-rw-r--r-- 1 user1 user1 20158 10月 15 14:18 mailstore/20221015141809279.eml
-rw-r--r-- 1 user1 user1  8710 10月 15 14:18 mailstore/20221015141811803.eml
-rw-r--r-- 1 user1 user1 14593 10月 15 14:18 mailstore/20221015141813379.eml
-rw-r--r-- 1 user1 user1 13624 10月 15 14:18 mailstore/20221015141814968.eml
-rw-r--r-- 1 user1 user1 13693 10月 15 14:18 mailstore/20221015141816531.eml

バウンスメールをsisimaiで解析し、MySQLに結果を登録する。

GitリポジトリのREADMEに記載されているサンプルスクリプト Bounced Mail Collect Script Example を使ってバウンスメールSisimaiで解析しMySQLに結果を登録します。

スクリプト内の MAIL_DIR を mailstore のパスに変更します。もしくは、https://gist.github.com/azumakuniyuki/3ad4b3a531b7ac6016e3f96232e3cc6a#file-update-sisito-db のようにMAIL_DIRを$*.shiftに変更して引数でパスを指定できるようにすることもできます。
mysql = Mysql2::Client.new()にあるusernameとpasswordをMySQLのCREATE USERでユーザーを作成したものに変更します。

変更後のスクリプトは以下のとおりです。

update-sisito-db.rb
#!/usr/bin/env ruby
require 'fileutils'
require 'sisimai'
require 'mysql2'
require 'tmpdir'

COLUMNS = %w(
  timestamp
  lhost
  rhost
  alias
  listid
  reason
  action
  subject
  messageid
  smtpagent
  softbounce
  smtpcommand
  destination
  senderdomain
  feedbacktype
  diagnosticcode
  deliverystatus
  timezoneoffset
  addresser
  recipient
)

MAIL_DIR = $*.shift

def process(path, **options)
  Dir.mktmpdir do |tmpdir|
    FileUtils.mv(Dir["#{path}/*"], tmpdir)
    v = Sisimai.make(tmpdir, **options) || []
    v.each {|e| yield(e) }
  end
end

def insert(mysql, data)
  values = data.to_hash.values_at(*COLUMNS)
  addresseralias = data.addresser.alias
  addresseralias = data.addresser.address if addresseralias.empty?
  values << addresseralias
  columns = (COLUMNS + ['addresseralias', 'digest', 'created_at', 'updated_at']).join(?,)
  timestamp = values.shift
  values = (["FROM_UNIXTIME(#{timestamp})"] + values.map(&:inspect) + ['SHA1(recipient)', 'NOW()', 'NOW()']).join(?,)
  sql = "INSERT INTO bounce_mails (#{columns}) VALUES (#{values})"
  puts 'SQL = ' + sql
  mysql.query(sql)
end

mysql = Mysql2::Client.new(host: 'localhost', username: 'bounce', password: 'user_password_1234', database: 'sisito_development')

process(MAIL_DIR) do |data|
  insert(mysql, data)
end

スクリプトを実行します。スクリプト実行が完了するとmailstore内のファイルはすべて削除されます。(二重取り込み防止のため?)ただし、処理途中でエラーが発生した場合でもすべて削除されるため注意が必要です。

$ ruby ./update-sisito-db.rb /home/user1/sisito/mailstore

sisitoを起動する。

ここでは、sisitoが受け付けるポート番号を1080番としています。
http://{このサーバーのIPアドレス}:1080/ にアクセスすると以下のような画面が表示されます。

$ bundle exec rails server -p 1080 -b 0.0.0.0

Web管理画面トップ
sisito Web管理画面

バウンスメール一覧
バウンス理由での絞り込みや宛先アドレスで検索できます。一覧では宛先アドレスが伏せ字になっていますが、 Show をクリックすると宛先アドレスが確認できます。
bounce一覧

Show のリンク先や Admin 画面にはBasic認証が設定されています。認証情報は、config/sisito.ymlに定義されています。

Docker版

Docker版は、docker-compose.ymlやDockerfileがあるディレクトリで以下のコマンドを実行することで起動できます。実行するとsisito-1, postfix-1, sisito_api-1, sisito-mysql-1というコンテナが起動します。

$ docker compose build
$ docker compose up

Webブラウザから http://localhost:3000/ にアクセスするとWeb管理画面が表示されます。
sisito-apiが含まれているため、Sisimaiを使ったバウンスメールの管理にある新バウンスメール管理システムで利用するための構成のようです。

DockerfileはDockerfile のベスト・プラクティスを参考にいろいろ削ったり足したりしています。

Rails 5.1.0版

本家リポジトリにあるものに対してDBスキーマの変更とmysql2 gemのバージョンのみ変更したものがこちらです。

Raspberry Pi 4B (Raspbian OS)で利用するには

上記のRails 5.1.0版をgit cloneし、mysql-server, libmysqlclient-devではなくmariadb-server, libmariadb-devをインストールします。

$ sudo apt-get update &&
  sudo apt-get upgrade &&
  sudo apt-get install -y --no-install-recommends \
      build-essential \
      libxml2-dev \
      libxslt-dev \
      mariadb-server \
      libmariadb-dev \
      nodejs \
      libsqlite3-dev \
      liblzma-dev \
      zlib1g-dev \
      libssl-dev \
      libreadline-dev \
      libyaml-dev \
      libxslt1-dev \
      software-properties-common \
      libffi-dev \
      tzdata \
      jq && \
  sudo apt-get clean && \
  sudo rm -rf /var/lib/apt/lists/*

MariaDBの初期設定後、ユーザーをパスワードを設定します。

$ sudo mariadb
GRANT ALL ON sisito_development.* TO 'bounce'@'localhost' IDENTIFIED BY 'user_password_1234' WITH GRANT OPTION;
GRANT ALL ON sisito_test.* TO 'bounce'@'localhost' IDENTIFIED BY 'user_password_1234' WITH GRANT OPTION;
GRANT ALL ON sisito_production.* TO 'bounce'@'localhost' IDENTIFIED BY 'user_password_1234' WITH GRANT OPTION;
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?