AWDwR "Chapter19 Action Mailer"

目標、年内読了!Chapter19行きますよ。

Sending E-mail

まずはメールを送信する設定をしなきゃね。development, testing, productionで同じ設定を使う場合はconfig/environment.rbに設定し、別々に設定したい場合はconfig/environments以下の設定ファイルに設定。

最初に設定しなきゃいけない項目は↓ :testはテスト用でメールは送信しない。:sendmailはローカルの/usr/sbin/sendmailでメールを送信する。:smtpSMTPでメールを送信する

ActionMailer::Base.delivery_method = :smtp | :sendmail | :test

:smtpを選んだ場合はさらにいくつかパラメタを設定する必要がある。

ActionMailer::Base.server_setting = {
  :address => "smtp.foo.com",  # SMTPサーバのホスト名。デフォルトはlocalhost。
  :port => 25,  # SMTPサーバのポート番号。デフォルトは25。
  :domain => "bar.com",  # 自分のドメイン名。
  :authentication => :login,  # :plain, :login, :cram_md5のどれかをとる。認証が不要であれば省略できる。
  :user_name => "dave",  # 認証時に使用するユーザ名。
  :password => "password",  # 認証時に使用するパスワード。
}

その他の設定オプションとしては以下のものがある。

ActionMailer::Base.perform_deliveries = true | false  # falseにするとメールを送信しない。
ActionMailer::Base.raise_delivery_errors = true | false  # trueにするとメール送信時にエラーが発生すると例外を上げる。
ActionMailer::Base.default_charset = "utf-8"  # メールで使用するキャラクタセット。


設定が終わったらいよいよメールの送信。Railsはジェネレータスクリプトでmailersを作ってくれるけどそんなことで驚いてちゃいけない。mailersはなんとapp/modelsの中のクラスとして作成される。

ruby script/generate mailer OrderMailer confirm sent

こうするとOrderMailerというクラスがapp/modelsにでき、2つのテンプレートファイルがapp/views/order_mailerにできる。

mailerクラスの各メソッドはインスタンス変数を設定することでメール送信の設定をする。

class OrderMailer < ActionMailer::Base
  def confirm(sent_at = Time.now)
    @subject = 'subject'  # サブジェクト
    @recipients = 'foo@bar.com'  # 受取人
    @cc = ''  # Cc:
    @bcc = ''  # Bcc:
    @from = 'bar@foo.com'  # From:
    @sent_on = sent_at  # Date: 省略時は現在時刻
    @charset = ''  # Content-Type: 省略時はserver_settingのdefault_charset、それもない場合はutf-8
    @headers = {}  # ヘッダのハッシュ
    @body = {}  # テンプレートに渡すハッシュ。
  end

  def sent(sent_at = Time.now)
    ...
  end
end


テンプレートファイルはフツーのERbのrhtmlファイルなので<%= %>なんかがそのまま使える。ただしrender()を使うときはパスに./で始まるパスを指定すること。@bodyに設定するハッシュはテンプレートからはインスタンス変数として扱える。具体的には@body["order"]とするとテンプレートからは@orderというインスタンス変数として見えるよ。


で、実際には今定義したメソッドを使うんじゃなくて、クラスメソッドのcreate_xxx(), deliver_xxx()を使う。xxxにはインスタンスメソッドの名前が入る。
deliver_xxx()は実際にメールを送信する。例えばconfirm()で定義したメールを送信する場合はこうなる。

OrderMailer.deliver_confirm(order)

一方、create_xxx()はメールの内容をTMail::Mailクラスのインスタンスとして返すだけで送信は行なわない。

class TestController < ApplicationController
  def create_order
    order = Order.find_by_name("Dave Thomas")
    email = OrderMailer.create_confirm(order)
    render(:text => "<pre>" + email.encoded + "</pre>") # encoded()はメールのテキストを返すメソッド
  end
end

HTMLメールを送信する場合はテンプレートでHTMLを生成し、set_content_type("text/html")でコンテントタイプをセットすればOK。

Receiving E-mail

Action Mailerを使えば受信したメールを処理するのも簡単。でもサーバからメールを取り出してアプリケーションに渡す処理がちょっと面倒かも。

メールを受信する場合はActionMailerクラスの中でreceive()メソッドを作成する。receive()メソッドは受信されたメールに相当するTMail::Mailオブジェクトを引数としてとる。

class IncomingTicketHandler < ActionMailer::Base
  def receive(email)
    ticket = Ticket.new
    ticket.from = email.from[0]
    ticket.report = email.body
    ticket.save
  end
end

で、問題は受信したメールをどうやって横取りしてこのreceive()メソッドに渡すか。
メールサーバの設定を変更する権限があればメール受信時にスクリプトを走らせて横取りするという方法があるけど、権限がない場合は.procmailrcにルールを書いてメールを横取りする方法もある。次に横取りしたメールをどうやってアプリケーションに渡すかなんだけどRailsのrunnerという機能を使って以下のようにできるよ。

ruby script/runner "IncomingTicketHandler.receive(STDIN.read)"

receive()クラスメソッドにメールのテキストを渡すと、パースしてTMail::Mailオブジェクトを生成し、receive()インスタンスメソッドに渡してくれる。

Testing E-mail

メールのテストにはユニットテストと機能レベルのテストがあるよ。

生成されるメールの内容を確認するユニットテストの場合は、さっきのジェネレータスクリプトを使うとtest/unit/order_mailer_test.rbというファイルが作成されるのでそれを使う。
テストで比較するメールの内容はtest/fixtures/order_mailer/以下に用意しておけば、read_fixture()メソッドで読み込んで、生成されたメールと比較してくれるんだけれども、一字一句比較する必要がなければ動的に生成される部分だけを比較するという方法もある。

class OrderMailerTest < Test::Unit::TestCase
  def test_confirm
    response = OrderMailer.create_confirm(@order)
    assert_equal("Subject", response.subject)
    assert_equal("bar@foo.com", response.to[0])
    assert_match(/Hello/, response.body)
  end
end

一方、メールがきちんと送信されるかどうかを確認する機能テストは、test環境の場合はAction Mailerはメールを送信せずActionMailer::Base.deliveriesという配列にメールの内容をセットするので、それを確認するようにすればよい。

class OrderControllerTest < Test::Unit::TestCase
  fictures :orders
  def setup
    @controller = OrderController.new
    @emails = ActionMailer::Base.deliveries
    @emails.clear
  end

  def test_confirm
    get(:confirm, :id => @dave_order.id)
    assert_equals(1, @emails.size)
    email = @emails.first
    assert_equal("Subject", response.subject)
    assert_equal("bar@foo.com", response.to[0])
    assert_match(/Hello/, response.body)
  end
end

機能テストを実行するにはrake test_functionalとするか、以下のスクリプトを実行すればOK。

ruby test/functional/order_controller_test.rb