主に言語とシステム開発に関して RSSフィード



2009-11-23

RailsでGMailを利用したメール送信 (ActionMailer + tlsmailの仕組みを理解しよう)

| はてなブックマーク -  RailsでGMailを利用したメール送信 (ActionMailer + tlsmailの仕組みを理解しよう) - 主に言語とシステム開発に関して


Ruby on Rails (1.2.6)で gmail からメール送信を行なうためのサンプルコードと,拡張クラスを作れるようになるための詳しい解説。

  • (1)ごくかんたんにSSL/TLS認証を使う場合(シンプル)

の2つの場合を考える。



(1)とにかくgmailで送信してみる

内容はほとんど固定でいいから,とりあえずGMailを使ってみる。

  • 本文:ビュー内で指定
  • 宛先:コントローラ内で指定
  • SMTP設定:コントローラ内で指定
  • 認証方式:SSL/TLS

(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 = 〜〜 のような代入形式で変数をセットしてもいい。

BCCとかは@bcc = 〜〜でセットできる。





(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

http://wota.jp/ac/?date=20050731

※メールテンプレート内で部分テンプレートを使いたい場合:

ActionMailer で部分テンプレートを使う方法

http://wota.jp/ac/?date=20071206


(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でメール送信

http://gendosu.ddo.jp/redmine/projects/rails/wiki/GMail%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6ActiveMailer%E3%81%A7%E3%83%A1%E3%83%BC%E3%83%AB%E9%80%81%E4%BF%A1


(1−3)動作に必要なライブラリを取得

コントローラ内には require 'tlsmail' というコードがあった。

ruby 1.8SSL認証のコードを動作させるためには,gemtlsmailダウンロードする必要がある。

コマンドラインから


>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/ActionMailerTLSを使ったメール送信

http://yakinikunotare.boo.jp/orebase/index.php?Ruby%20on%20Rails%2FActionMailer%A4%C7TLS%A4%F2%BB%C8%A4%C3%A4%BF%A5%E1%A1%BC%A5%EB%C1%F7%BF%AE



(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)基礎編のまとめと補足

ここまでで,アプリの構成案をまとめると,

  1. メール送信内容入力アクションでは,
    1. ビュー上のformで,件名などメールに反映させたい項目をユーザに入力させ
    2. メール送信実行アクションにPOST。
  2. メール送信実行アクションでは,
    1. メール本文の設定として,FORMからのパラメータをparams[]で解釈し,
    2. 必要に応じてSSL/TLSを有効に切り替えた上で
    3. SMTPの設定として,メールアカウント関連の設定項目を収集して ActionMailer::Base.server_settings にセットし,
    4. Mailerクラスに各種設定を渡してdeliver_メソッドを呼び出す。
  3. Mailerクラスは,
    1. fromとかrecipientsなど,メール送信に関わる設定項目をセットし,
    2. メール本文に関わる設定(埋め込み変数など)を,メールテンプレートに渡す
    3. そして送信実行する。
  4. メールテンプレートは,
    1. Mailerから渡された埋め込み変数を,送信実行時に展開する。

という手順になる。


把握のためのポイント:

  • 3種類の設定項目があり,それぞれ適用対象が異なる。混同しないように。
    • メール送信サーバに関わる設定(送信実行前に,コントローラやenvironment.rbでセット)
    • メールヘッダに関わる設定(Mailerでセット)
    • メール本文に関わる設定(Mailer経由でテンプレートにセット)
  • 通常 Model > Controller > View の向きに情報が流れる。しかしActionMailerを使った構成は,見かけ上, Controller > Model > View の向きに見える。(ActionMailerからメールテンプレート変数を渡すので。)混乱しないように。

補足事項として,ActionMailerのつくりについて。

知りたい事は

RAILS_ROOT\vendor\rails\actionmailer\lib\action_mailer\base.rb

の中を見ればわかる。

  • Notifierの中に定義されているメール送信(の準備の)アクションの事をnotificationと呼んだりする。

create(メールオブジェクトの作成)とdeliver(メール配信実行)の違いについて:


Notifierに対してcreate_hogeが呼ばれると,

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
384    def initialize(method_name=nil, *parameters) #:nodoc:
385      create!(method_name, *parameters) if method_name 
386    end
  • create!はhogeを呼ぶ(下記コードのsendの部分)。その際,create_hoge()の引数hogeに渡す。
390    def create!(method_name, *parameters) #:nodoc:
391      initialize_defaults(method_name)
392      send(method_name, *parameters)
440      @mail = create_mail

また,Notifierに対してdeliver_hogeが呼ばれると,

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_deliveries
561      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)に加えて,付加的な処理を行なってみる。

  1. 日本語が文字化けしないようにする
  2. 件名などの情報もテンプレート内で定義する

(2−1)文字化け対策

(1)で学んだcreate!メソッドを再定義して,メール情報のエンコード処理を付加すればよい。

そのようなクラスとして JpMailer などを公開して下さっている方がいる。

下記のクラスを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文字化けを防ぐため。

nkfオプション仕様は下記ページを参照。

文字コードを変換できるコマンドnkfのオプション一覧

http://it.kndb.jp/entry/show/id/745


そして,自分が作った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で、メールのヘッダもビューに書けるようにする

http://d.hatena.ne.jp/koseki2/20080425/actionmailerView


コントローラ側を書きなおし


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でエラーをハンドリングする

http://d.hatena.ne.jp/sai-ou89/20080814


補足:TLSに関する説明

rubyTLS対応させるためのライブラリとして利用している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 サービス拡張

http://hori.homelinux.net/rfc/rfc3207-ja.txt


また,TLS/SSLの原理的なところは下記ページや暗号理論の参考書を参照。

SSL入門

http://www005.upp.so-net.ne.jp/nakagami/Memo/SSL/