Rubyでeventmachineを使って高速にメールを送る

またまた某サービスで、今度はメールを送るという要件が発生。
しかも、ユーザごとに異なった内容を送るというもの。


テンプレートはErubisを使うとして、
メール送信はどのライブラリを使おう。tmailかなぁ?
Google検索すると、eventmachineってものがあるらしい。
Rubyから非同期でガンガンSMTPサーバに接続できるようです。ひゃっほい!


というわけで、ためしにeventmachineを使って
メールを送信するテストコードを書いてみた。
一応並列に20個コネクションを張っている…はず。
テストが怪しいので、ご利用時は検証のこと。
eventmachineの挙動全く理解してねっす。
eventmachineにはSMTP clientだけでなくSMTP serverの実装もあるので、
テスト時にはそれを使うといいと思う。


ちなみに、EM::Protocols::SmtpClient.sendでGoogle検索したら
2件くらいしか引っかからない…
情報少ないなぁ。

#!/usr/bin/ruby
# UTF-8前提です
$KCODE = 'u'

require 'nkf'
require 'eventmachine'

class SendMailWithEventMachine
  attr_reader :successes, :errors

  def send_mail(domain, host, port, starttls, from, queue_threshold, &blk)
    @successes = 0
    @errors = 0
    @on_queue = 0
    @all_queued = false

    @domain = domain
    @host = host
    @port = port
    @starttls = starttls
    @from = from

    EM.run {
      queue_threshold.times {
        send_mail_queue(blk)
      }
    }
  end

  private

  def send_mail_queue(blk)
    unless @all_queued
      if data = blk.call
        if (smtp = EM::Protocols::SmtpClient.send :domain => @domain,
          :host => @host,
          :port => @port,
          :starttls => @starttls,
          :from => @from,
          :to => [data[:to]],
          :header => {'To' => data[:to],
                      'Subject' => NKF.nkf('-jW --mime', data[:subject])},
          :body => NKF.nkf('-jW', data[:body]),
          :verbose => false)
          @on_queue += 1
          smtp.callback {|r|
            @on_queue -= 1
            @successes += 1
            send_mail_queue(blk)
          }
          smtp.errback {|e|
            @on_queue -= 1
            @errors += 1
            send_mail_queue(blk)
          }
        end
      else
        @all_queued = true
      end
    end
    if @all_queued and @on_queue == 0
      EM.stop_event_loop
    end
  end
end

mails = [{
  :to => 'victim1@example.com',
  :subject => 'さっき天一で',
  :body => '無料券使ったら',
},
{
  :to => 'victim2@example.com',
  :subject => '並盛だけですと言われ',
  :body => 'きちんと抗議したよ!',
}]

i = 0
sm = SendMailWithEventMachine.new
sm.send_mail(
  'example.com', 'mx1.example.com', 25, false, 'spammer@example.com', 20) {
  # mysqlのfetch_rowなどを想定
  next if i >= mails.length
  i += 1
  mails[i - 1]
}