はじめに
はじめまして、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