AWDwR "Chapter19 Action Mailer"
目標、年内読了!Chapter19行きますよ。
Sending E-mail
まずはメールを送信する設定をしなきゃね。development, testing, productionで同じ設定を使う場合はconfig/environment.rbに設定し、別々に設定したい場合はconfig/environments以下の設定ファイルに設定。
最初に設定しなきゃいけない項目は↓ :testはテスト用でメールは送信しない。:sendmailはローカルの/usr/sbin/sendmailでメールを送信する。:smtpはSMTPでメールを送信する
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