ActionMailerで、メールのヘッダもビューに書けるようにする。

以下はRails 1.2.3の話。2.0でどうなってるかは知らないです。もっと良くなってるかも。

概略

From: info@example.com
To: <%= @user.email %>
Bcc: admin@example.com
Subject: <%= @user.name %>さんにお知らせです。
Content-Transfer-Encoding: 7bit

<%= @user.name %>さん

○○サービスのご利用ありがとうございます。
云々。

みたいなrhtmlを使ってメールを送信する。

背景

ActionMailerのrhtmlは、通常のコントローラでrenderされる場合と動作が違う。

  • 明示的に指定しないとヘルパーが使えない。
  • url_forの:only_path => falseが動作しない。

ヘルパーを有効にする(ドキュメント化されていない)方法は、

に載っていた。ActionMailer::Baseを継承したモデル内で、

helper :application

と書くとApplicationHelperが有効になる。これも何か不安だが、次のurl_forの問題が大きかった。

<%= url_for(:action=> "list", :only_path => false) %>

のように書くとエラーになる。:hostを指定しないと、:only_path => falseにできないみたいだった。

ActionViewのurl_forは:only_pathがデフォルトでtrueなので、falseを指定しないと、スキームやホスト、ポート番号が出力されない。http://exmpale.com/list ではなく、/list になってしまう。

メールにURLを載せたいことはよくあるのに、これでは困ってしまう。


改善

コントローラ内で render_to_string を使ってメール本文を作成して、これをActionMailerに渡す。

ついでに、メールのヘッダをビューに書けるようにした。Subjectをビューに書けないのはヘンだ。

下のsimple_mailer.rbをmodelsに追加。

class SimpleMailer < ActionMailer::Base

  FIELDS = %w{from to cc bcc subject date content-type}

  def simple(src)
    fields = parse src

    from fields["from"]
    recipients fields["to"]
    cc fields["cc"]
    bcc fields["bcc"]
    subject 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 do |name, value|
      headers name => value
    end
  end
  
  def parse(src)
    src = src.gsub(/(?:\r\n|\r)/, "\n")
    lines = src.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
  private :parse

  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
  private :set_field_value

end

コントローラで次のようにしてメールを送信する。

def send_notifier
  src = render_to_string(:action => "mail_notifier", :layout => false)
  SimpleMailer.deliver_simple(src)
end
private :send_notifier

mail_notifier.rhtml で宛先などを指定する。

From: info@example.com
To: <%= @user.email %>, abc@example.com
Cc: address1@example.com
Cc: address2@example.com
Bcc: admin@example.com
Subject: 件名です。
Content-Transfer-Encoding: 7bit

本文です。

from, to, bc, subject, date, content-typeは大文字でも小文字でも可。それ以外はheadersにそのまま渡される。

アドレスは複数指定可。複数行にわけるか、またはコンマでつなぐ。

そのままだとutf-8で送られてしまうので、gettextを使うなどしてください。