2009-11-23
RailsでGMailを利用したメール送信 (ActionMailer + tlsmailの仕組みを理解しよう)
Ruby on Rails (1.2.6)で gmail からメール送信を行なうためのサンプルコードと,拡張クラスを作れるようになるための詳しい解説。
- (1)ごくかんたんにSSL/TLS認証を使う場合(シンプル)
の2つの場合を考える。
(1)とにかくgmailで送信してみる
内容はほとんど固定でいいから,とりあえずGMailを使ってみる。
(1−1)Mailerクラスとメールテンプレートを作る
まずコマンドプロンプトから雛型を生成。
>ruby script/generate mailer MyMailer :0:Warning: Gem::SourceIndex#search support for Regexp patterns is deprecated exists app/models/ create app/views/my_mailer exists test/unit/ create test/fixtures/my_mailer create app/models/my_mailer.rb create test/unit/my_mailer_test.rb
ActionMailerを継承したクラスが生成される。
models/my_mailer.rbの中に,下記のようにメソッドを書く。
class MyMailer < ActionMailer::Base # hogeという設定でメール送信 def hoge( recipient ) # 宛先 recipients recipient # 件名 subject "[WELCOME] #{recipient}" # 送信者 from "fuga@gmail.com" # 自分のgmailアドレスを記載 # 本文に埋め込みたい変数 body :recipient => recipient, :now => Time.now end end
解説:
Mailerクラス内の一つのメソッドが,一つのメールテンプレートに対応する。
後述するが,コントローラから呼び出す際には,メソッド名にdeliver_を付けて
MyMailer.deliver_hoge( "atesaki@mail.com" )
とすれば送信実行される。
引数は自由に決めてよい。
ここでは宛先メールアドレスをhogeメソッド内に渡している。他にも
- 件名
- 送信元
- メール本文内に埋め込みたい変数
などを渡す事になるだろう。
変数の渡し方は下記サイトのサンプルなどを参考に。
AWDwR "Chapter19 Action Mailer"
http://d.hatena.ne.jp/enkimi/20051221#1135183029
recipients 〜〜 のようなメソッド形式で変数をセットしてもいいし,
@recipients = 〜〜 のような代入形式で変数をセットしてもいい。
(1−2)メールテンプレートを作る
Mailer内のhogeメソッドは,メール本文のレンダリングのためにhoge.rhtmlを利用する。
app\views\my_mailer\hoge.rhtml として下記を保存:
Hello there, Mr. <%= @recipient %> --- # received at <%= @now %>
メール本文で使っている変数には,Mailerのhogeメソッド内のbodyメソッドで渡された値が使われる。
※メール本文の出典:
優しいRailsの育て方 Action Mailer
ActionMailer で部分テンプレートを使う方法
(1−2)Controllerを作成する
雛型を生成
>ruby script\generate controller mailtest :0:Warning: Gem::SourceIndex#search support for Regexp patterns is deprecated exists app/controllers/ exists app/helpers/ create app/views/mailtest exists test/functional/ create app/controllers/mailtest_controller.rb create test/functional/mailtest_controller_test.rb create app/helpers/mailtest_helper.rb
コントローラ内に下記のようなメソッドを作る
class MailtestController < ApplicationController # メール送信実行アクション def send_mail_exec # SSL/TLSを有効に require 'tlsmail' Net::SMTP.enable_tls( OpenSSL::SSL::VERIFY_NONE ) # SMTPサーバ利用時のオプション ActionMailer::Base.server_settings = { :address => 'smtp.gmail.com', :port => '587', :user_name => 'gmailのメールアドレスの@より前の部分', :password => 'gmailのパスワード', :authentication => :login } # Mailerのhogeメソッド内の設定でメール送信を実行 MyMailer.deliver_hoge( "atesaki@gmail.com" ) # 送信先アドレスを渡す end end
メール本文にかかわる設定はMailerクラス内に書いたのに対し,
メール送信サーバ(SMTP)にかかわる設定は,このようにコントローラ内に書く事ができる。
そうすれば,user_nameとかpasswordなどの設定項目をDBに格納しておいて,コントローラ内でアクション実行の際に動的に指定することができる。
※動的に設定するのではなくて,全コントローラ内で共通のサーバ設定を固定で利用したい場合は,設定ファイル(config/environment.rb) に書いてもよいだろう。
GMailを使ってActiveMailerでメール送信
(1−3)動作に必要なライブラリを取得
コントローラ内には require 'tlsmail' というコードがあった。
ruby 1.8 でSSL認証のコードを動作させるためには,gemでtlsmailをダウンロードする必要がある。
コマンドラインから
>gem install tlsmail Successfully installed tlsmail-0.0.1 1 gem installed Installing ri documentation for tlsmail-0.0.1... Installing RDoc documentation for tlsmail-0.0.1...
インストール先を確認
>gem which tlsmail (checking gem tlsmail-0.0.1 for tlsmail) D:/dev/ruby/lib/ruby/gems/1.8/gems/tlsmail-0.0.1/lib/tlsmail.rb
これでOK。
もしLinux上なら
gem which tlsmail | grep rb | xargs file
としてファイル情報を調べてみよう。
なお,webrickを再起動しないとgemの追加は反映されない。
※なお古い案として,Net::SMTPを直接書き変えるためのプラグインを手動で作るという方法もある。
でもこの方法は,productモードで動作しなくなったり色々不安がある。
なので,今回のようにtlsmailを使った方がよい。
Ruby on Rails/ActionMailerでTLSを使ったメール送信
(1−4)動作確認
ブラウザで
http://localhost:3000/mailtest/send_mail_exec
にアクセス。
Template is missing の画面が出ればメール送信成功。(send_mail_exec.rhtmlを作っていないので)
log/development.logを見てみよう。
Processing MailtestController#send_mail_exec (for 127.0.0.1 at 2009-11-23 12:05:08) [GET] Session ID: 8738d2154639a5ea58b34e903d3b2c81 DEPRECATION WARNING: server_settings has been renamed smtp_settings, this warning will be removed with rails 2.0 See http://www.rubyonrails.org/deprecation for details. (called from send_mail_exec at D:/prog/rails/rals_mailtest/app/controllers/mailtest_controller.rb:12) Sent mail: From: fuga@gmail.com To: atesaki@gmail.com Subject: [WELCOME] atesaki@gmail.com Mime-Version: 1.0 Content-Type: text/plain; charset=utf-8 Hello there, Mr. atesaki@gmail.com --- # received at Mon Nov 23 12:05:08 +0900 2009 ActionController::MissingTemplate (Missing template D:/prog/rails/rals_mailtest/app/views/mailtest/send_mail_exec.rhtml): ... Rendering D:/prog/rails/rals_mailtest/vendor/rails/actionpack/lib/action_controller/templates/rescues/layout.rhtml (500 Internal Error)
送信されたメールの本文を確認することができる。
(1−5)基礎編のまとめと補足
ここまでで,アプリの構成案をまとめると,
という手順になる。
把握のためのポイント:
- 3種類の設定項目があり,それぞれ適用対象が異なる。混同しないように。
- 通常 Model > Controller > View の向きに情報が流れる。しかしActionMailerを使った構成は,見かけ上, Controller > Model > View の向きに見える。(ActionMailerからメールテンプレートに変数を渡すので。)混乱しないように。
補足事項として,ActionMailerのつくりについて。
知りたい事は
RAILS_ROOT\vendor\rails\actionmailer\lib\action_mailer\base.rb
の中を見ればわかる。
- 今回,ActionMailer::Baseを継承したMailerクラスとして MyMailer を作成したが,こういった子クラスの事をNotifierという。
- Notifierの中に定義されているメール送信(の準備の)アクションの事をnotificationと呼んだりする。
create(メールオブジェクトの作成)とdeliver(メール配信実行)の違いについて:
Notifierに対してcreate_hogeが呼ばれると,
- method_missing経由で,ActionMailerのインスタンスが作られ,.mailでメールオブジェクトが返り値として返却される。(.mailは,@mailのアクセッサとして327行目で定義されている。)
327 attr_reader :mail ... 330 def method_missing(method_symbol, *parameters)#:nodoc: 331 case method_symbol.id2name 332 when /^create_([_a-z]\w*)/ then new($1, *parameters).mail
- インスタンス化にはcreate!が伴う。
384 def initialize(method_name=nil, *parameters) #:nodoc: 385 create!(method_name, *parameters) if method_name 386 end390 def create!(method_name, *parameters) #:nodoc: 391 initialize_defaults(method_name) 392 send(method_name, *parameters)
- hogeは,メール内容として各種インスタンス変数をセットする。@bodyとか@recipientsとか。これがcreate_hogeの結局の目的である。
- hogeによってセットされたインスタンス変数に基づき,create_mail内でTMailオブジェクトが構築され,最終的な返り値となる。
440 @mail = create_mail
また,Notifierに対してdeliver_hogeが呼ばれると,
- method_missing経由で,ActionMailerのインスタンスが作られる。前述のとおりインスタンス化にはcreate!が伴うから,メールオブジェクトの作成も済まされる。そして,.deliver!で配信が実行される。
330 def method_missing(method_symbol, *parameters)#:nodoc: 331 case method_symbol.id2name 333 when /^deliver_([_a-z]\w*)/ then new($1, *parameters).deliver!
- deliver!内では,送信方式しだいで分岐が起こる。SMTP経由ならばperform_delivery_smtpが呼ばれる。
446 def deliver!(mail = @mail) 451 send("perform_delivery_#{delivery_method}", mail) if perform_deliveries561 def perform_delivery_smtp(mail) 565 Net::SMTP.start(smtp_settings[:address], smtp_settings[:port], smtp_settings[:domain], 566 smtp_settings[:user_name], smtp_settings[:password], smtp_settings[:authentication]) do |smtp| 567 smtp.sendmail(mail.encoded, mail.from, destinations) 568 end
かいつまんで言えば
- create_hogeの目的は,hogeメソッドをキックして,正しくメール情報を格納した@mailを作ること
という事になる。
これらの情報を押さえておけば,拡張していく事も容易だ。
ActionMailerを継承した子クラス内でcreate!とかを再定義し,メール情報を加工したうえで本来の処理を行なえばよいのだから。
(2)応用編
(1)に加えて,付加的な処理を行なってみる。
(2−1)文字化け対策
(1)で学んだcreate!メソッドを再定義して,メール情報のエンコード処理を付加すればよい。
そのようなクラスとして JpMailer などを公開して下さっている方がいる。
- http://cyushi.blogspot.com/2008/04/rails_22.html
- http://www.digo.jp/top/?p=115#
- http://wota.jp/ac/?date=20050731 (iso2022jpMailer)
下記のクラスをjp_mailer.rbとしてlib以下に配置。
require 'nkf' class JpMailer < ActionMailer::Base # これがないと "Content-Type: charset=utf-8" になる @@default_charset = 'iso-2022-jp' # MIME Encode def self.mime_encode(str_utf8) str_iso2022jp = NKF.nkf("-W -j -m0 --cp932'", str_utf8) str_base64 = [str_iso2022jp].pack('m').chomp "=?iso-2022-jp?B?#{str_base64}?=" end # MIME Decode def self.mime_decode(str_mime) NKF.nkf('-J -m --utf8',str_mime) end # bodyをiso-2022-jpへ変換 def create!(*) super @mail.body = NKF.nkf("-W -j --cp932'", @mail.body) return @mail end end
※@@default_charsetを指定しているのは,Thunderbirdで文字化けを防ぐため。
そして,自分が作ったNotifierはActionMailer::Baseではなく,JpMailerを継承するように書き換える。
class MyMailer < ActionMailer::Base
↓
class MyMailer < JpMailer
件名指定部には文字コード変換処理を追加。(UTF8Nで保存)
subject "[WELCOME] #{recipient}"
↓
subject JpMailer.mime_encode( "[ようこそ] #{recipient}" )
これだけで,メールテンプレートや件名内に日本語(UTF8)を書くことが可能に。
前項でcreate!が@mailを処理している事を理解済みなので,こういった拡張クラスの動作も把握しやすくなる。
(2−2)件名等の情報をテンプレート内で定義
ブラウザに通常のHTMLを表示する場合,ビューファイル内には
- ページ本体
- ページタイトル(title要素)
などを記述できる。
なお,タイトルはlayoutに記述することが多いが,render :layout => false すれば,1個のrhtmlファイル内にタイトル要素も含めることが可能だ。
だからメール送信の場合も当然,viewフォルダ内に設置するメールテンプレート内に
- メール本体
- メールタイトル(つまり件名)
の両方を書けてよさそうなものだ。
これまでのActionMailerの使い方だと,件名などのヘッダ情報は Mailerクラス内で定義しなければならない。
そこを,テンプレート内で定義できるように改造してみよう。
参考:
ActionMailerで、メールのヘッダもビューに書けるようにする
コントローラ側を書きなおし
class MailtestController < ApplicationController # メール送信実行アクション(改訂) def send_mail_exec # SSL/TLSを有効に require 'tlsmail' Net::SMTP.enable_tls( OpenSSL::SSL::VERIFY_NONE ) # SMTPサーバ利用時のオプション ActionMailer::Base.server_settings = { :address => 'smtp.gmail.com', :port => '587', :user_name => 'gmailのメールアドレスの@より前の部分', :password => 'gmailのパスワード', :authentication => :login } # メールのソーステキストを作成 @from_address = "自分のメールアドレス" @to_address = "atesaki_to@mail.com" # これらのインスタンス変数を使って本文がレンダリングされる mail_source = render_to_string( :template => "my_mailer/template_1", :layout => false ) # Mailerのhogeメソッドにソーステキストを解釈させ,メール送信を実行 MyMailer.deliver_hoge( mail_source ) end end
解説:
メールテンプレート:
views/my_mailer/template_1.rhtml
From: <%= @from_address %> To: <%= @to_address %> Bcc: atesaki_bcc@example.com Subject: ようこそ <%= @to_address %> さん Content-Transfer-Encoding: 7bit ようこそ <%= @to_address %> さん etc
Mailerには,空行以降を本文として認識させることにする。
Mailerクラスを書き直し
class MyMailer < JpMailer FIELDS = %w{from to cc bcc subject date content-type} # メールテンプレートに件名等の情報が書いてある場合の送信準備処理を行ないます def hoge( mail_source ) # 各フィールドの値を取得 fields = parse_fields( mail_source ) # 各フィールドの値をセット from fields["from"] recipients fields["to"] cc fields["cc"] bcc fields["bcc"] subject JpMailer.mime_encode( fields["subject"] ) body fields["body"] sent_on fields["date"] unless fields["date"].nil? content_type fields["content-type"] unless fields["content-type"].nil? # ヘッダ内のその他のフィールドの値をセット fields["headers"].each{ |k, v| headers k => v } end private # メールのソーステキストから,各フィールドの値を抽出します。 def parse_fields( mail_source ) mail_source = mail_source.gsub( /(?:\r\n|\r)/, "\n" ) lines = mail_source.split( /\n/ ) fields = { "headers" => {} } until lines.empty? line = lines.shift if line.strip.empty? fields[ "body" ] = lines.join( "\n" ) break end (name, value) = line.split( /:\s+/, 2 ) next if value.to_s.empty? name.strip! if FIELDS.include?( name.downcase ) set_field_value( fields, name.downcase, value ) else fields[ "headers" ][ name ] = value end end return fields end # フィールド値を正しいフォーマットで格納します。 def set_field_value( fields, name, value ) if fields[ name ].is_a?( Array ) fields[ name ] << value elsif fields[ name ].is_a?( String ) fields[ name ] = [ fields[ name ], value ] else fields[ name ] = value end end end
前項の通り,Mailer内のnotificationメソッドの役目は「メール情報を自分のインスタンス変数として格納すること」だから,その格納の仕方を少し変えてやればよいということだ(parse_fields)。
これで,送信されるメールの情報を1ファイル(template_1.rhtml)だけで持ちまわることが可能になる。
(2−3)メール送信失敗時にエラーメッセージを出す
これはいたって簡単。コントローラ側を書き換えるだけ。
notificationメソッドがhogeの場合のサンプル:
# メール送信実行アクション def send_mail_exec # SSL/TLSを有効に require 'tlsmail' Net::SMTP.enable_tls( OpenSSL::SSL::VERIFY_NONE ) # SMTPサーバ利用時のオプション ActionMailer::Base.server_settings = { :address => 'smtp.gmail.com', :port => '587', :user_name => 'gmailのメールアドレスの@より前の部分', :password => 'gmailのパスワード', :authentication => :login } # エラー検出を有効にする ActionMailer::Base.raise_delivery_errors = true begin MyMailer.deliver_hoge( ) rescue Exception => e e_msg = e.message.gsub( "\n", "" ) flash[ :error ] => "メール送信に失敗しました。" + "<a href='#' onclick='alert(\"#{ e_msg }\")'>詳細...</a>" end end
raise_delivery_errorsを明示的にtrueにしておくと,メール送信失敗時に例外がスローされる。
ActionMailerでエラーをハンドリングする
補足:TLSに関する説明
rubyをTLS対応させるためのライブラリとして利用しているtlsmailの中身は,下記から閲覧できる。
Koders Code Search : tlsmail / smtp.rb
http://www.koders.com/ruby/fid4B733F71E091A3127D8B3FA17C4F0E6D81DB8830.aspx#L463
下記のように,TLSが有効 ( @use_tls == true ) な場合の処理が Net::SMTP に追加されている。
451 def do_start(helodomain, user, secret, authtype) 454 s = timeout(@open_timeout) { TCPSocket.open(@address, @port) } 455 @socket = InternetMessageIO.new(s) ... 463 if @use_tls 465 context = OpenSSL::SSL::SSLContext.new 466 context.verify_mode = @verify ... 476 s = OpenSSL::SSL::SSLSocket.new(s, context) 478 starttls 479 s.connect 480 logging 'TLS started'
sという変数がソケット。
- 454行目で,メールサーバ(ポートはSMTP.default_portにある通り25番)に対していったん普通のTCPソケットを開いてから,
- 476行目でSSL接続としてsを定義しなおし,
- 478行目でSTARTTLSコマンドを発行し,(参考:http://www.drive.ne.jp/func/mail08.html )
- 479行目で正式にSSL接続を開始している。
これがtlsmailの仕組みだ。
ところで,コントローラ側でtlsmailを利用する際に,OpenSSL::SSL::VERIFY_NONE と書いてあるのに違和感を覚えたかもしれない。
これは別に「SSLを利用しない」という意味ではない。
SSLはちゃんと利用するが,その際のモードとして「サーバから送られてくる証明書のCAを検証した時,検証に失敗しても,メールサーバとの通信を継続する」という意味だ。
開発時はこれで問題なし。
Ruby 1.9.1 リファレンスマニュアル constant OpenSSL::SSL::VERIFY_NONE
http://doc.okkez.net/191/view/method/OpenSSL=SSL/c/VERIFY_NONE
anonymous cipher を用いない場合にサーバーは証明書を送ってきます。TLS/SSL ハンドシェイクの結果は SSL_get_verify_result を使ってチェックできます。証明書の検証の結果によらずハンドシェイクは継続します。
Ruby 1.9.1 リファレンスマニュアル constant OpenSSL::SSL::VERIFY_PEER (デフォルト)
http://doc.okkez.net/191/view/method/OpenSSL=SSL/c/VERIFY_PEER
サーバーの証明書を検証します。検証が失敗した場合、TLS/SSL ハンドシェイクを即座に終了させます。サーバーが証明書を返さずに、anonymous cipher を用いる場合、VERIFY_PEER は無視されます。
TLSの「ハンドシェイク」とは何なのか,メールサーバとどのようにやり取りがなされるかの詳細については,下記ページ等を参照。
telnetでメールを送信する
http://www.nurs.or.jp/~telnet/smtp.html
RFC3207: Transport Layer Security を用いたセキュアな SMTP の為のSMTP サービス拡張
また,TLS/SSLの原理的なところは下記ページや暗号理論の参考書を参照。
SSL入門
- 35 http://www.google.co.jp/search?hl=ja&client=firefox-a&rls=org.mozilla:ja:official&q=ralis メール+bcc&btnG=検索&lr=lang_ja&aq=f&oq=
- 20 http://www.google.co.jp/search?sourceid=navclient&hl=ja&ie=UTF-8&rlz=1T4ACAW_jaJP344JP332&q=Rails+ActionMailer+SSL/TLS
- 12 http://www.google.co.jp/search?hl=ja&safe=off&client=firefox-a&rls=org.mozilla:ja:official&hs=QaD&q=raise_delivery_errors+メールの送信先&btnG=検索&lr=lang_ja&aq=f&oq=
- 11 http://www.google.co.jp/search?hl=ja&source=hp&q=rails+メール本文 整形 一あわせ&lr=&aq=f&oq=
- 10 http://www.google.co.jp/search?hl=ja&lr=lang_ja&client=firefox-a&rls=org.mozilla:ja:official&hs=M0I&q=gmail+送信 コマンド&start=20&sa=N
- 8 http://blog.goo.ne.jp/shanshan2006/e/337ce0df2f5b5ab435fec7d9f1d64496?fm=rss
- 8 http://www.google.co.jp/search?hl=ja&q=rails+メール&sourceid=navclient-ff&rlz=1B3GGGL_jaJP284JP285&ie=UTF-8&aq=h
- 8 http://www.google.co.jp/search?q=RailsでGmail&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:ja:official&hl=ja&client=firefox-a
- 8 http://www.google.co.jp/search?sourceid=chrome&ie=UTF-8&q=rails+gmail
- 7 http://www.google.co.jp/search?hl=ja&lr=lang_ja&tbs=lr:lang_1ja&q=コマンドプロンプト メール送信+gmail&aq=f&aqi=&aql=&oq=&gs_rfai=