Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

JavaMailでHTMLメールとTEXTメールを同時送信する方法と注意

新規事業開発室 新卒1年目の山邊です。 Sansan の新規事業で2020年5月11日にリリースされた「あらゆる請求書をオンラインで受け取る。」ことの出来るサービスBill Oneを開発しています。
その過程でメールを送信する際にHTML形式とTEXT形式をまとめて一つのメールで送信する方法を学びました。 実際のコードと共に注意点を踏まえてまとめたいと思います。

HTML + TEXTメールを配信する理由

メールはテキスト形式に代わり、表現力豊かなHTML形式が一般的になってきています。一方でメーラーや受信者の環境によって、HTML形式を表示しない場合があります。
1通のメールの中で両方の形式があれば受信者側が好きな方を選んで閲覧する事ができます。その為、HTML形式で書かれているHTMLパートとTEXT形式で書かれているTEXTパートを両方含むメールを送信する方がより良いでしょう。

Content-Type

Content-Typeはメールヘッダ部に設定し、メールの各要素の形式を表します。種類がいくつかあり、Content-TypeについてはRFC2045、MultipartについてはRFC2046によって仕様が決められています。
メーラーはContent-Typeによって形式を判断する為、適切に設定する必要があります。メールによく使用されるContent-Typeについてまとめておきます。

Content-Type 意味
"text/plain" Text形式 単にテキストのメール
"text/html" HTML形式 HTML形式のメール
"multipart/alternative" 複数の要素を含み、その要素が表す内容が同一である HTMLパート + Textパートを含むパート
"multipart/mixed" 複数の異なる要素を含む 添付ファイルを含むパート
"multipart/related" 複数の関連性の強い要素を含む 絵文字など

これらの要素は入れ子にする事で複数設定する事ができます。今回のHTML+TEXTメールの場合は以下のような形式です。

- multipart/alternative
    - text/plain
    - text/html

他の例についてはmultipartなメールの構造は通常どの程度まで複雑な入れ子になるかに分かりやすく書いてあったので、そちらを参照してください。 

実際のコード

以下はJavaMail(1.6.2)を使用し、Kotlinで書かれたHTMLパートとTEXTパートを含むメール送信の基本です。コードはシンプルで、直感的に理解できるかもしれませんが、その他の設定項目や注意する点がいくつかあります。

val smtpHost = "<SMTPサーバーのホスト>"
val smtpPort = "<SMTPサーバーのポート>"
val smtpUser = "<SMTPサーバーのユーザー>"
val smtpPassword = "<SMTPサーバーのパスワード>"

val props = Properties().apply {
    setProperty("mail.smtp.host", smtpHost)
    setProperty("mail.smtp.port", smtpPort)
    setProperty("mail.smtp.auth", "true")
    setProperty("mail.smtp.starttls.enable", "true")
}

val session = Session.getInstance(props, object : Authenticator() {
    override fun getPasswordAuthentication(): PasswordAuthentication {
        return PasswordAuthentication(smtpUser, smtpPassword)
    }
})

val textBody = "<メール本文(テキスト版)>"
val htmlBody = "<メール本文(HTML版)>"
val mailFrom = "<メールの送り主>"
val mailTo   = "<メールの送り先>"
val subject  = "<メール題名>"

val multipart = MimeMultipart("alternative")
multipart.addBodyPart(MimeBodyPart().apply {
    setContent(textBody, "text/plain; charset=UTF-8") 
})
multipart.addBodyPart(MimeBodyPart().apply { 
    setContent(htmlBody, "text/html; charset=UTF-8") 
})

val message = MimeMessage(session).apply {
    setFrom(mailFrom)
    setRecipients(MimeMessage.RecipientType.TO, mailTo)
    setSubject(subject, "UTF-8")
    setContent(multipart)
}

Transport.send(message)
    

解説と注意

セッションの作成

val props = Properties().apply {
    setProperty("mail.smtp.host", smtpHost)
    setProperty("mail.smtp.port", smtpPort)
    setProperty("mail.smtp.auth", "true")
    setProperty("mail.smtp.starttls.enable", "true")
}
val session = Session.getInstance(props, object : Authenticator() {
    override fun getPasswordAuthentication(): PasswordAuthentication {
        return PasswordAuthentication(smtpUser, smtpPassword)
    }
})

SMTPサーバーに接続する為の情報を設定します。プロパティには数多くの種類があるので、必要に応じて設定しましょう。今回は基本的に必要になる最小限の項目を設定しています。 他の項目についてはJavaMail API documentationから確認できます。

コンテントの作成

val multipart = MimeMultipart("alternative")
multipart.addBodyPart(MimeBodyPart().apply { 
    setContent(textBody, "text/plain; charset=UTF-8") 
})
multipart.addBodyPart(MimeBodyPart().apply { 
    setContent(htmlBody, "text/html; charset=UTF-8") 
})

複数の項目を含むコンテントを作成する場合 MimeMultipart() を使用します。

注意1 MimeMultipartには引数を渡す

MimeMultipartは引数なしの場合はContent-Typeが "multipart/mixed" となります。今回のように "multipart/alternative" を設定したい場合は引数に "alternative" を渡す必要があります。
また、注意3で説明しますが、setHeader() でContent-Typeを設定した場合でも、MimeMultipartをsetContent()した場合、MimeMultipartのContent-Typeが優先されるので注意が必要です。

注意2 addBodyPartの順番が重要

"multipart/alternative" の場合、後ろに表記されているものが優先されます。これはRFC2046 (5.1.4) によって決められています。
HTMLパートとTEXTパートを含むメールでHTMLを優先して表示してほしい場合は "text/html" を含むコンテントを "text/plain"より後ろに追加する必要があります。
ただ、Yahooメールの場合は順番に関係なくHTMLパートを優先してくれるようです。動作確認の際は他のメーラーでも確認する必要があります。

メッセージを作成しメールを送信

val message = MimeMessage(session).apply {
    setFrom(mailFrom)
    setRecipients(MimeMessage.RecipientType.TO, mailTo)
    setSubject(subject, "UTF-8")
    setContent(multipart)
}

Transport.send(message)

最後に MimeMessage() でメッセージを作成し、Transport.send() で送信します。

注意3 setHeader()を過信しない

MimeMessageには setHeader() がありContent-Typeを設定できます。しかし、以下のようなコードではsetHeader()で設定したContent-Typeが上書きされ、Multipartのもつ "multipart/mixed" が使用されます。
Content-Typeをしたい時は setHeader() よりもMimeMultipart() で設定する方が良いでしょう。

// 良くない例
val multipart = MimeMultipart() // 引数無し=> "multipart/mixed"
multipart.addBodyPart(MimeBodyPart().apply { setContent(textBody, "text/plain; charset=UTF-8") })
multipart.addBodyPart(MimeBodyPart().apply { setContent(htmlBody, "text/html; charset=UTF-8") })

val message = MimeMessage(session).apply {
    setFrom(mailFrom)
    setRecipients(MimeMessage.RecipientType.TO, mailTo)
    setSubject(subject, "UTF-8")
    setHeader("content-type", "multipart/alternative")
    setContent(multipart)
}

Transport.send(message)

まとめ

JavaMailでHTMLパートとTEXTパートを含むメールを送信する場合には以下の注意が必要です。

  • MimeMessageのsetHeader()でContent-Typeを設定するのではなく、MimeMultipartに引数を渡し、Content-Typeを設定する
  • addBodyPartの順番が重要であり、HTMLパートをTEXTパートよりも後に追加する


buildersbox.corp-sansan.com

buildersbox.corp-sansan.com

buildersbox.corp-sansan.com

© Sansan, Inc.