こんにちは、tahara です。
Rails システムでのバウンスメール処理ってどうするのがいいんでしょう? ベステプラクティスではないかもしれませんが、弊社でのバウンスメール処理方法を書いてみたいと思います。
まずメーラークラスで return_path にバウンスメール受信用のアドレスを指定します。
class Notifier < Jpmobile::Mailer::Base # ActionMailer::Base include BouncedMailFilter default(from: 'from@expamle.com', return_path: 'bounce@example.com') ... end
上記メーラークラスで include しているクラスでは、 バウンスメール DB にメールアドレスが登録されているかチェックして、 登録済みのアドレスにはメールを送信しないようにしています。
module BouncedMailFilter def mail(*args) m = super return m unless m.to.size == 1 email = m.to.first if not_deliver?(email) m.perform_deliveries = false end m end private def not_deliver?(email) return true if email =~ /@example.com\z/ BouncedMail.where(email: email).exists? end end
return_path に指定したメアドの ~/.forward で、バウンスメールを ruby に食べさせるようにします。 ここで rails runner とか使おうとすると遅すぎて話にならないので、こんな方式にしています。 またそれと同時に admin@example.com にも転信して、普通にバウンスメールを受信できるようにしておきす。
admin@example.com "| ruby /var/www/outing/current/bin/mailforward.rb"
ruby に食べさせたメールは次のコードで Rails アプリに POST されます。
#!/usr/bin/env ruby require 'net/https' class MailForward def self.post(mail) http = Net::HTTP.new('localhost', 11223) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE request = Net::HTTP::Post.new('/bounced_mails') request.set_form_data({ mail: mail }) response = http.start { http.request(request) } if response.code.to_i == 200 return 0 else return 1 end end end exit(MailForward.post(STDIN.read))
Rails アプリは POST されたバウンスメールを読んで、 配信できなかったメールアドレスを上記の BouncedMailFilter でチェックしている DB に登録します。
「バウンスメールを読んで」のところが問題で、SMTP サーバによってバウンスメールの中味が違ったりします。 とりま、次のようなコードで処理しています。 完全に検出することはできませんが、運用上問題ないくらいには検出できていると思われます。
class BouncedMail < ActiveRecord::Base def self.receive(mail) analyzer = BounceAnalyzer.new(mail) if analyzer.bounced? unless BouncedMail.where(email: analyzer.email).exists? BouncedMail.create!(email: analyzer.email, reason: analyzer.reason) end end end class BounceAnalyzer attr_reader :email def initialize(raw_mail) @mail = Mail.new(raw_mail) if @mail.parts.size == 3 @summary_part = Mail.new(@mail.parts[0].body) @status_part = Mail.new(@mail.parts[1].body) @email = parse_email(@status_part.body.to_s) end end def bounced? case when !@email false when domain_reject? false when user_unknown?, host_not_found?, in_reply_to_rcpt_to? true else false end end def reason @status_part.body.to_s end private def domain_reject? @mail.body.to_s =~ /in\s+reply\s+to\s+end\s+of\s+DATA\s+Command/mi end def in_reply_to_rcpt_to? @summary_part.body.to_s =~ /in\s+reply\s+to\s+RCPT\s+TO\s+command/mi end def host_not_found? @status_part.body.to_s =~ /Host\s+or\s+domain\s+name\s+not\s+found/mi end def parse_email(body) case body when /Original-Recipient:\s*(?:rfc822;)?\s*(.+@.+)/i $1 when /Final-Recipient:\s*(?:rfc822;)?\s*(.+@.+)/i $1 else nil end end def user_unknown? case @status_part.body.to_s when /Status:\s*5\.1\.1/i, /User unknown/i, /Unknown user/i true else false end end end end
うーん、きれいじゃないですね。 きれいな gem の出現を待ちます。 でも、こんなコードでもバウンスメールは大はばに減りました (^_^)v