TalentX Tech Blog

Tech Blog

RFC 5322に基づいた、メールの送信者名に特殊記号を含む場合のアドレス解析方法

はじめに

はじめまして、TalentX バックエンドエンジニアの伊東です! 採用MAサービス MyTalentの開発を担当しています。

MyTalentのバックエンドではGo言語を使用しており、メールサーバーから受け取ったメールのアドレスをパースする際にはnet/mailパッケージのParseAddress関数を使用しています。 そのParseAddress関数にて、特定の記号(カンマ , やアットマーク @ など。※1)を含む送信者名の場合にエラーが発生しました。

本記事では、このエラーの原因と解決方法をRFC 5322の仕様に基づいて解説します。

※1 : 具体的には ()<>@\,." を指します。本記事では「特殊記号」と呼びます。

ParseAddress関数について

ParseAddressはRFC 5322に則ってアドレスを解析し、Address構造体へと変換します。 Address構造体のNameフィールドがアドレス名と対応します。

基本的な使い方は以下の通りです。

addr, err := mail.ParseAddress("伊東太郎 <taro.itou@example.com>")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Name: %s, Email: %s\n", address.Name, address.Address)
// → Name: 伊東太郎, Email: taro.itou@example.com

こちらはParseAddress関数とAddress構造体のソースコードです。

// ParseAddress parses a single RFC 5322 address, e.g. "Barry Gibbs <bg@example.com>"
func ParseAddress(address string) (*Address, error) {
   return (&addrParser{s: address}).parseSingleAddress()
}

// Address represents a single mail address.
// An address such as "Barry Gibbs <bg@example.com>" is represented
// as Address{Name: "Barry Gibbs", Address: "bg@example.com"}.
type Address struct {
   Name    string // Proper name; may be empty.
   Address string // user@domain
}

コードコメントにもあるとおり、引数のaddressにはBarry Gibbs <bg@example.com>といった形式の文字列を期待しています。 このような形式の文字列を、RFC 5322ではname-addrとして定義しています。(詳しくは後述。)

エラーが発生する状況

冒頭に記載しましたParseAddress関数でのエラーはどういう状況だと発生するのかを説明します。

メールクライアントによっては送信者名を任意の文字列へと変更でき、例えばGmailではこちらでその方法が説明されています。 そうして送信者名を伊東太郎, taro itoと変更した場合、送信されたメールを受け取ったアプリケーションサーバーでParseAddress関数を実行するとエラーが発生します。

// 送信者名にカンマを含むためエラーになります
addr, err := mail.ParseAddress("伊東太郎, taro itou <taro.itou@example.com>")
if err != nil {
    log.Fatal(err)
    // → mail: missing '@' or angle-addr
}

その他には、氏名の前に会社名や部署名などを記載してコロン : などで区切ったりする場合や、氏名に加えメールアドレスを記載したりする(アットマーク @ が含まれる)場合なども該当します。

これはアドレス名に特殊記号が含まれているためなのですが、なぜこの場合にはエラーとなるのか、その原因を探っていきます。

RFC 5322について

RFC(Request for Comments)とはインターネットの技術標準を記した文書であり、その内容により更に細分化されています。そのうちの1つ、RFC 5322ではメールアドレスのフォーマットに関する定義をしています。

name-addr

前述のname-addr(例:Barry Gibbs <bg@example.com>)については以下のフォーマットと定義されています。

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

見慣れない表現かもしれませんが、こういった記法をABNF(Augmented Backus-Naur Form)記法といいます。(ちなみにABNF記法はRFC5234で規定されています。)

上記のname-addrは次のような意味になります。

name-addrは「先頭に記載された任意のdisplay-nameと、(必須の)angle-addrとの組み合わせ」である。

display-name

今回はメールのアドレス名を対象としますので、name-addrを構成する要素のうち、display-nameやその関連要素の定義について更に確認します。

display-name = phrase

phrase = 1*word / obs-phrase

word = atom / quoted-string

上記の定義より、display-nameは次のような意味になります。(obs-phraseはレガシーな構文を表しています。)

display-nameは「1つ以上のatomもしくはquoted-stringから成るもの」。

さらに、ABNF表記は割愛しますが、display-nameを構成するatomとquoted-stringは次のような意味になります。

atomは「アルファベット(A-Z, a-z)、数字(0-9)、atomで使用可能な指定の記号(!#$%&'*+-/=?^_`{|}~)から成る単語」。
quoted-stringは「ダブルクォートで囲まれた、ダブルクォート`"`とバックスラッシュ`\`以外から成る文字列」。

特殊記号を含むアドレス名でエラーが発生する理由とその対策

以上を踏まえると、ダブルクォートで囲まれていない限り文字列はatomとして扱われるため、特殊記号を含む文字列はRFC違反となりエラーが発生するということが定義から理解できました。

そこで、これらの記号が含まれている場合にはダブルクォートで囲うことで、定義に即した形式で変換できるはずです。 その方針での実装例が以下です。

実装例

package mail

import (
   "fmt"
   "net/mail"
   "strings"
)


var (
   ErrInvalidFormat     = fmt.Errorf("invalid email address format")
   ErrMultipleAddresses = fmt.Errorf("multiple email addresses are not supported")
)


func Parse(address string) (*mail.Address, error) {
   // 1. まずは標準(net/mail)のParseAddressを試す
   addr, err := mail.ParseAddress(address)
   if err == nil {
       return addr, nil
   }

   /* 2. 複数アドレスが渡されていないかチェック
      この関数は単体のmail.Address構造体を返すためのものなので、
      複数のアドレスをパースするParseAddressList関数にて処理できないこと(エラーが起きること)を確認し、
      以下のような入力を弾く
      - "Alice <alice@example.com>, Bob <bob@example.com>"
      - "A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;"
   */
   addrs, err := mail.ParseAddressList(address)
   if err == nil {
       return nil, fmt.Errorf("%w: %v", ErrMultipleAddresses, addrs)
   }

   // 3. アドレス名をquoted-stringへ変換した上でパースする
   return parseQuotedName(address)
}


func parseQuotedName(address string) (*mail.Address, error) {
   start := strings.LastIndex(address, "<")
   end := strings.LastIndex(address, ">")

   // "<"や">"が無い場合や逆順の場合はエラーを返す
   if start == -1 || end == -1 || start >= end {
       return nil, fmt.Errorf("%w: missing or misplaced angle brackets", ErrInvalidFormat)
   }

   email := strings.TrimSpace(address[start+1 : end])
   if email == "" {
       return nil, fmt.Errorf("%w: empty email address", ErrInvalidFormat)
   }
   name := strings.TrimSpace(address[:start])
   if name != "" {
       // 名前に引用符を追加する
       name = fmt.Sprintf("\"%s\"", name)
   }

   // メールアドレスが不正な場合に対応するためにParseAddress実行が必要
   return mail.ParseAddress(fmt.Sprintf("%s <%s>", name, email))
}

結果

このParse関数を使うことにより、以下の結果を得ることができます。(Parse関数の引数addressの入力例を箇条書きに記載しています。)

正常に実行できる入力例

Alice <alice@example.com>
\"Alice\" <alice@example.com>
<Alice> <alice@example.com>
alice@example.com
Alice , <alice@example.com>(標準のParseAddressではエラーになる)
Alice : <alice@example.com>(標準のParseAddressではエラーになる)
alice@example.com <alice@example.com>(標準のParseAddressではエラーになる)

不正値と判定する入力例

Alice <alice.com>(メールアドレスが不正)
Alice <alice@example.com>, Bob <bob@example.com>(複数は想定していない)
A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;(グループアドレスは想定していない)
・空文字

まとめ

今回の調査によって、アドレス名に特殊記号が含まれていた場合にエラーが起きる原因と、その裏付けとなるRFC 5322の仕様を理解することができました。

また、その仕様に基づいた、特殊記号に対応したアドレス解析を行う実装例を紹介しました。

最後に

最後までお読みいただきありがとうございました!

TalentXでは一緒に働く仲間を募集しております。 ご興味ある方は下記リンクの求人をご覧ください! talentx.brandmedia.i-myrefer.jp

カジュアル面談も行っていますのでぜひご応募ください。 i-myrefer.jp