Action Mailerでfromフィールドに差出人名を表示したい

よく見かけるAction Mailer のサンプルはこんな感じだと思います。

mail from: 'noreply@example.com',
     to: 'foobar@example.com',
     subject: 'Hi'

このケースでは差出人は noreply@example.com となるんですが、時にサービス名などを設定したくなることもあると思います。 今回はActionMailerでどうやるといいの? って話です。

ドキュメント

まずはrails/actionmailer at master · rails/rails · GitHubを探しますが、特に方法は書かれていません。

ActionMailerはMailのラッパーなので、GitHub - mikel/mail: A Really Ruby Mail Libraryに目を通すと、それらしきサンプルが見つかります。

mail = Mail.new do
  to      'nicolas@test.lindsaar.net.au'
  from    'Mikel Lindsaar <mikel@test.lindsaar.net.au>'
  subject 'First multipart email sent with Mail'
end

"Mikel Lindsaar"の部分を置き換える形で運用を始めたところ、ごく稀にエラーが発生してメールが送信できない問題にぶつかりました。

An ArgumentError occurred in *****#create:

  An SMTP From address is required to send a message. Set the message smtp_envelope_from, return_path, sender, or from address.
  app/controllers/*****.rb:123:in `****************'

原因

ArgumentErrorはmail/check_delivery_params.rb at df48a05a7fb5a4271e6df12da7afb26a53494a18 · mikel/mail · GitHubにてraiseされていました。smtp_envelope_from がblankだとこのエラーが発生します。

いろいろとテストした結果、少なくとも ドット「.」や括弧「(」「)」が表示名に含まれている場合にエラーが再現することが確認できました。

name = 'kotaroi.(test)'

mail = Mail.new do
  from    "#{name} <noreply@example.com>"
  to      'foobar@example.com'
  subject 'This is a test email'
end

mail.smtp_envelope_from # => nil

ということで、rfcを読み解きます。本来は運用前に調べておくべき話ですね(汗

RFC

obsoleteやupdateがたくさんあるので、どれを読むべきか把握するのが大変でした。。 今回読むべきは RFC 5322 - Internet Message Format で、FromフィールドはこのRFCで規定されています。

3.6.2.  Originator Fields

   The originator fields of a message consist of the from field, the
   sender field (when applicable), and optionally the reply-to field.
   The from field consists of the field name "From" and a comma-
   separated list of one or more mailbox specifications.  

と記載されており、セクションを少し遡ると、

3.4.  Address Specification

   Addresses occur in several message header fields to indicate senders
   and recipients of messages.  An address may either be an individual
   mailbox, or a group of mailboxes.

   address         =   mailbox / group

   mailbox         =   name-addr / addr-spec

   name-addr       =   [display-name] angle-addr

   angle-addr      =   [CFWS] "<" addr-spec ">" [CFWS] /
                       obs-angle-addr

   group           =   display-name ":" [group-list] ";" [CFWS]

   display-name    =   phrase

   mailbox-list    =   (mailbox *("," mailbox)) / obs-mbox-list

と書かれています。最終的に

   atext           =   ALPHA / DIGIT /    ; Printable US-ASCII
                       "!" / "#" /        ;  characters not including
                       "$" / "%" /        ;  specials.  Used for atoms.
                       "&" / "'" /
                       "*" / "+" /
                       "-" / "/" /
                       "=" / "?" /
                       "^" / "_" /
                       "`" / "{" /
                       "|" / "}" /
                       "~"

   atom            =   [CFWS] 1*atext [CFWS]

   dot-atom-text   =   1*atext *("." 1*atext)

   dot-atom        =   [CFWS] dot-atom-text [CFWS]

   specials        =   "(" / ")" /        ; Special characters that do
                       "<" / ">" /        ;  not appear in atext
                       "[" / "]" /
                       ":" / ";" /
                       "@" / "\" /
                       "," / "." /
                       DQUOTE

   qtext           =   %d33 /             ; Printable US-ASCII
                       %d35-91 /          ;  characters not including
                       %d93-126 /         ;  "\" or the quote character
                       obs-qtext

   qcontent        =   qtext / quoted-pair

   quoted-string   =   [CFWS]
                       DQUOTE *([FWS] qcontent) [FWS] DQUOTE
                       [CFWS]

(中略)


   word            =   atom / quoted-string

   phrase          =   1*word / obs-phrase

に辿り着きました。

これにより、表示名(display-name)は quoted-string でない場合は specials を含めてはならないことを確認できました。裏を返せば、specials を使いたい場合は quoted-string とすればよいです。

次は「Ascii以外はどうするの」という疑問が出てきますが、RFC 6532 - Internationalized Email Headers にてUTF-8が使えるよう拡張されています。

3.2.  Syntax Extensions to RFC 5322


   The preceding changes mean that the following constructs now allow
   UTF-8:

   1.  Unstructured text, used in header fields like "Subject:" or
       "Content-description:".

   2.  Any construct that uses atoms, including but not limited to the
       local parts of addresses and Message-IDs.  This includes
       addresses in the "for" clauses of "Received:" header fields.

   3.  Quoted strings.

   4.  Domains.

コード

specials が含まれるか否かを判定し、含まれる場合は quoted-string(つまりダブルクォートで囲む)とし、さらにダブルクォートとバックスラッシュをエスケープすればよさそうです。

def envelope_display_name(display_name)   
  name = display_name.dup

  # Special characters
  if name && name =~ /[\(\)<>\[\]:;@\\,\."]/
    # escape double-quote and backslash
    name.gsub!(/\\/, '\\')
    name.gsub!(/"/, '\"')

    # enclose
    name = '"' + name + '"'
  end

  name
end


name = envelope_display_name('display name here')

mail from: "#{name} <noreply@example.com>",
     to: 'foobar@example.com',
     subject: 'Hi'

日本語(UTF8-non-ascii)が含まれていても、問題なく動作することを確認済みです。

まとめ

ActionMailerで差出人名を変更する方法を調べました。 本当は mail gem の実装まで確認しておいたほうがいいんですが、時間が限られてるのでまた今度。。