"); // (4)
helper.setTo(user.getEmailAddress()); // (5)
helper.setSubject("Registration confirmation."); // (6)
String cid = "identifier1234";
String text = "
Hi "
+ user.getUserName()
+ ", welcome to EXAMPLE.COM!\r\n
"
+ "If you were not an intended recipient, Please notify the sender.";
helper.setText(text, true); // (7)
ClassPathResource res = new ClassPathResource("image/logo.jpg");
helper.addInline(cid, res); // (8)
}
});
// omitted
}
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | \ ``JavaMailSender``\ をインジェクションする。
* - | (2)
- | \ ``JavaMailSender``\ の\ ``send``\ メソッドを利用してメールを送信する。
| 引数には\ ``MimeMessagePreparator``\ を実装した匿名内部クラスを定義する。
* - | (3)
- | 文字コードを指定して、\ ``MimeMessageHelper``\ のインスタンスを生成する。
| この例では、文字コードにUTF-8を指定している。
| \ ``MimeMessageHelper``\ のコンストラクタの第二引数に\ ``true``\ を指定することで、マルチパートモードになる。
* - | (4)
- | Fromヘッダの内容を設定する。
* - | (5)
- | Toヘッダの内容を設定する。
* - | (6)
- | Subjectヘッダの内容を設定する。
* - | (7)
- | 本文の内容を設定する。\ ``setText``\ メソッドの第二引数に\ ``true``\ を指定することで、Content-Typeがtext/htmlになる。
* - | (8)
- | インラインリソースのコンテンツIDを指定してインラインリソースを設定する。
| この例では、\ ``identifier1234``\ というコンテンツIDで、クラスパス上にある\ :file:`image/logo.jpg`\ というファイルを設定している。
.. note::
\ ``addInline``\ メソッド\ は、\ ``setText``\ メソッドの後に呼び出すこと。そうしないと、メールクライアントがインラインリソースを正しく参照できないことがある。
|
メール送信時の例外について
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| \ ``JavaMailSender``\ の\ ``send``\ メソッドを利用してメール送信を行う際に発生する例外は\ ``org.springframework.mail.MailException``\ を継承した例外である。
| \ ``MailException``\ を継承した例外クラスと、それぞれの例外の発生条件について、以下の表に示す。
.. tabularcolumns:: |p{0.10\linewidth}|p{0.35\linewidth}|p{0.55\linewidth}|
.. list-table:: \ **メール送信時の例外**\
:header-rows: 1
:widths: 10 35 55
* - 項番
- 例外クラス
- 発生条件
* - 1.
- \ :url_spring_javadoc:`MailAuthenticationException `\
- | 認証失敗時に発生する。
* - 2.
- \ :url_spring_javadoc:`MailParseException `\
- | メールメッセージのプロパティに不正な値が設定されている場合に発生する。
* - 3.
- \ :url_spring_javadoc:`MailPreparationException `\
- | メールメッセージを作成中に想定外のエラーが起きた場合に発生する。
想定外のエラーとしては、例えばテンプレートライブラリで発生するエラーといったものがある。
| \ ``MimeMessagePreparator``\ で発生した例外が\ ``MailPreparationException``\ にラップされてスローされる。
* - 4.
- \ :url_spring_javadoc:`MailSendException `\
- | メールの送信エラーが起きた場合に発生する。
.. note::
特定の例外に対するエラー画面遷移については、\ :doc:`../WebApplicationDetail/ExceptionHandling`\ を参照されたい。
|
How to extend
--------------------------------------------------------------------------------
テンプレートを使用したメール本文の作成方法
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
上で示した実装例のようにJavaソースでメール本文を直接組み立てるのは、以下の理由から推奨しない。
* メール本文をJavaソースで組み立てるのは可読性が悪くエラーを作りやすい。
* 表示ロジックとビジネスロジックの境界が曖昧となる。
* メール本文のデザインを変更するために、Javaソースの修正、コンパイル、デプロイが必要になる。
| よって、メール本文のデザインを定義するためにテンプレートライブラリを使用することを推奨する。
| 特にメール本文が複雑になるような場合はテンプレートライブラリを使用すべきである。
|
FreeMarkerを使用したメール本文の作成
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
本ガイドラインでは、テンプレートライブラリとして\ :url_freemarker:`FreeMarker >`\ を使用する方法について説明する。
* FreeMarkerを使用するために、依存ライブラリを設定する。
\ **pom.xmlの設定例**\
.. code-block:: xml
org.freemarker
freemarker
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | FreeMarkerのライブラリをdependenciesに追加する。
* \ ``freemarker.template.Configuration``\ を生成するためのFactoryBeanをBean定義する。
\ **Bean定義ファイルの設定例**\
.. tabs::
.. group-tab:: Java Config
.. code-block:: java
@Bean("freemarkerConfiguration")
public FreeMarkerConfigurationFactoryBean freemarkerConfiguration() {
FreeMarkerConfigurationFactoryBean bean = new FreeMarkerConfigurationFactoryBean(); // (1)
bean.setTemplateLoaderPath("classpath:/META-INF/freemarker/"); // (2)
bean.setDefaultEncoding("UTF-8"); // (3)
return bean;
}
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | \ ``FreeMarkerConfigurationFactoryBean``\ をBean定義する。
* - | (2)
- | \ ``templateLoaderPath``\ プロパティにテンプレートファイルの格納された場所を指定する。
| この例では、クラスパス上にある\ :file:`META-INF/freemarker/`\ ディレクトリを設定している。
* - | (3)
- | \ ``defaultEncoding``\ プロパティにデフォルトのエンコードを指定する。
| この例では、UTF-8を設定している。
.. group-tab:: XML Config
.. code-block:: xml
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | \ ``FreeMarkerConfigurationFactoryBean``\ をBean定義する。
* - | (2)
- | \ ``templateLoaderPath``\ プロパティにテンプレートファイルの格納された場所を指定する。
| この例では、クラスパス上にある\ :file:`META-INF/freemarker/`\ ディレクトリを設定している。
* - | (3)
- | \ ``defaultEncoding``\ プロパティにデフォルトのエンコードを指定する。
| この例では、UTF-8を設定している。
.. note::
上記以外の設定については、\ :url_spring_javadoc:`FreeMarkerConfigurationFactoryBeanのJavaDoc `\ を参照されたい。
FreeMarker自体の設定については、\ :url_freemarker:`FreeMarker Manual (Programmer's Guide / The Configuration) `\ を参照されたい。
* メール本文のテンプレートファイルを作成する。
\ **テンプレートファイルの設定例**\
.. code-block:: text
<#escape x as x?html> <#-- (1) -->
Hi ${userName}, welcome to Macchinetta!
<#-- (2) -->
If you were not an intended recipient, Please notify the sender.
#escape>
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | XSS攻撃への対策としてHTMLエスケープを行うように設定している。
* - | (2)
- | データモデルに設定された\ ``userName``\ の値を埋め込む。
.. note::
テンプレート言語(FTL)の詳細については、\ :url_freemarker:`FreeMarker Manual (Template Language Reference) `\ を参照されたい。
* テンプレートを使用してメール本文を生成し、メール送信する。
\ **Javaクラスの実装例**\
.. code-block:: java
@Inject
JavaMailSender mailSender;
@Inject
Configuration freemarkerConfiguration; // (1)
public void register(User user) {
// omitted
mailSender.send(new MimeMessagePreparator() {
@Override
public void prepare(MimeMessage mimeMessage) throws Exception {
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,
StandardCharsets.UTF_8.name());
helper.setFrom("EXAMPLE.COM ");
helper.setTo(user.getEmailAddress());
helper.setSubject("Registration confirmation.");
Template template = freemarkerConfiguration
.getTemplate("registration-confirmation.ftl"); // (2)
String text = FreeMarkerTemplateUtils
.processTemplateIntoString(template, user); // (3)
helper.setText(text, true);
}
});
// omitted
}
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | \ :url_freemarker:`Configuration `\ をインジェクションする。
* - | (2)
- | \ ``Configuration``\ の\ ``getTemplate``\ メソッドを利用して\ :url_freemarker:`Template `\ を取得する。
| この例では、テンプレートファイルとして"registration-confirmation.ftl"を指定している。
* - | (3)
- | 取得した\ ``Template``\ をもとに、\ ``org.springframework.ui.freemarker.FreeMarkerTemplateUtils``\ の\ ``processTemplateIntoString``\ メソッドを利用してテンプレートから文字列を生成する。
| この例では、データモデルとして\ ``userName``\ プロパティを持つ\ ``User``\ オブジェクト(JavaBeans)を指定している。これにより、テンプレートファイルの\ ``${userName}``\ の箇所に\ ``userName``\ プロパティの値が埋め込まれる。
|
Appendix
--------------------------------------------------------------------------------
.. _email-iso-2022-jp:
ISO-2022-JPのエンコードについての考慮
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. note::
ここではUTF-8に対応していない場合の例を記載しているが、UTF-8に対応しているのであればUTF-8を使用することを検討されたい。
| 日本語のメールを送信する際、メールクライアントがUTF-8に対応していない場合は一部の文字を変換して送る必要がある。
| 例えば、MS932で入力された文字列に対しエンコードにISO-2022-JPをはじめとするJIS X 0208の文字集合をベースとしたエンコードを設定した場合、以下の表に記載する7文字において文字化けが発生する。
.. tabularcolumns:: |p{0.20\linewidth}|p{0.10\linewidth}|p{0.15\linewidth}|p{0.15\linewidth}|p{0.15\linewidth}|p{0.20\linewidth}|
.. list-table::
:header-rows: 2
:widths: 20 15 15 15 15 20
* - 変換前
-
-
- 変換後
-
-
* - | MS932
| 入力文字
- | 入力値
| (SJIS)
- | Unicode
| (UTF-16)
- | Unicode
| (UTF-16)
- | ISO-2022-JP
| (JIS)
- | JIS X 0208
| 代替文字
* - | ―(全角ハイフン)
- | 815D
- | U+2015
- | U+2014
- | 213E
- | —(EM ダッシュ)
* - | -(ハイフンマイナス)
- | 817C
- | U+FF0D
- | U+2212
- | 215D
- | −(全角マイナス)
* - | ~(全角チルド)
- | 8160
- | U+FF5E
- | U+301C
- | 2141
- | 〜(波ダッシュ)
* - | ∥(平行記号)
- | 8161
- | U+2225
- | U+2016
- | 2142
- | ‖(双柱)
* - | ¢(全角セント記号)
- | 8191
- | U+FFE0
- | U+00A2
- | 2171
- | ¢(セント記号)
* - | £(全角ポンド記号)
- | 8192
- | U+FFE1
- | U+00A3
- | 2172
- | £(ポンド記号)
* - | ¬(全角否定記号)
- | 81CA
- | U+FFE2
- | U+00AC
- | 224C
- | ¬(否定記号)
この問題は、Unicodeを介して文字コード変換を行う際に、MS932に有りJIS X 0208に無い文字が存在するためであり、文字化けを回避するためには、文字化けする文字について代替文字に文字コードを置き換えるなどの対処を行う必要がある。
以下に、変換処理の実装例を示す。
.. code-block:: java
public static String convertISO2022JPCharacters(String targetStr) {
if (targetStr == null) {
return null;
}
char[] ch = targetStr.toCharArray();
for (int i = 0; i < ch.length; i++) {
// @formatter:off
ch[i] = switch (ch[i]) {
case '\u2015' -> '\u2014'; // '―'(全角ハイフン) -> '—'(EM ダッシュ)
case '\uff0d' -> '\u2212'; // '-'(ハイフンマイナス) -> '−'(全角マイナス)
case '\uff5e' -> '\u301c'; // '~'(全角チルド) -> '〜'(波ダッシュ)
case '\u2225' -> '\u2016'; // '∥'(平行記号) -> '‖'(双柱)
case '\uffe0' -> '\u00A2'; // '¢'(全角セント記号) -> '¢'(セント記号)
case '\uffe1' -> '\u00A3'; // '£'(全角ポンド記号) -> '£'(ポンド記号)
case '\uffe2' -> '\u00AC'; // '¬'(全角否定記号) -> '¬'(否定記号)
default -> ch[i];
};
// @formatter:on
}
return String.valueOf(ch);
}
.. note::
Unicodeへのマッピング時の問題であるため、入力値の文字コードに依らず変換は必要である。
変換対象となるのは日本語を含む文字列が設定される可能性のあるヘッダおよび本文の文字列である。
日本語を含む可能性があり一般的によく使われると考えられるヘッダとしては、From、To、Cc、Bcc、Reply-To、Subjectが挙げられる。
| また、エンコードにISO-2022-JPを設定する場合、以下のような範囲外となる拡張文字が文字化けするため、これらの拡張文字は使用すべきではない。
| 拡張文字を代替文字に変換してもよい場合、前述した7文字と同様にアプリケーションで独自に変換を行う方法を検討されたい。
.. figure:: ./images_Email/EmailOutofEscapeCharacter.png
:alt: Out of EscapeCharacter
:width: 100%
:align: center
\ **図-範囲外となる拡張文字の例**\
|
JavaMailで発生していたマルチバイト文字を使用する際の不具合について
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
JavaMailでは、送信するメールの本文の終端がマルチバイト文字で終わっていると、終端に余計な文字(「?」や「w)」等)が出力される場合があり、従来は以下の方法で回避していた。
* メール本文の終端文字を半角文字にする
* メール本文の終端を改行コード(CRLF)にする
.. tip::
本事象は、シングルバイト文字とマルチバイト文字の切り替えのために付与される制御コードが付与されていなかったことに起因し、JavaMail 1.4.4でワークアラウンドが施されたことによって、以降のバージョンでは当事象が発生しなくなった。
|
.. _email-header-injection:
メールヘッダ・インジェクション対策
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| メールヘッダ・インジェクション攻撃が成功すると、本来意図していない宛先にメール送信され、迷惑メール送信の踏み台に悪用される可能性がある。
| メールヘッダ(Subject等)の内容に外部から入力された文字列を利用する場合、メールヘッダ・インジェクション攻撃への対策が必要となる。
|
| 例えば、\ ``MimeMessageHelper``\ の\ ``setSubject``\ メソッドで以下の文字列を設定すると、Bccヘッダを追加し本文を改ざんすることが可能となる。
.. code-block:: text
Notification\r\nBcc: attacker@exapmle.com\r\n\r\nManipulated body.
メールヘッダ・インジェクション攻撃への対策としては、以下のような方法が考えられる。
* メールヘッダに設定する内容は固定値とし、外部から入力された文字列はすべてメール本文に出力する。
* メールヘッダに設定する内容に改行文字が含まれないことをチェックする。
|
.. _email-processing-method:
処理方式
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| メール送信は時間のかかる処理であるため、Webアプリケーションのリクエストの中で送信処理を行うと応答時間が長くなってしまう。このため、通常はWebアプリケーションのリクエストの中では送信処理を行わず、非同期でメール送信を行う処理方式とすることが多い。
| メール送信の処理方式について詳細については言及しないが、以下に一例を示すので参考にされたい。
|
データベースまたはメッセージキューに保持されたメール情報をもとにメール送信を行う
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
データベースまたはメッセージキューに保持されたメール情報をもとにメール送信を行うには、以下のような機能をアプリケーションに組み込む。
* 送信するメールの情報(宛先や本文、添付ファイル等)をデータベース(またはメッセージキュー)に登録する。
* データベース(またはメッセージキュー)から未送信のメール情報を定期的に取得し、SMTPによるメール送信を行う。
* 送信結果をデータベース(またはメッセージキュー)に登録する。
なお、以下の点を含めて検討する必要がある。
* 登録されたメール情報やメール送信結果の確認方法
* メール送信エラー時の取り扱い
.. tip::
メールサービスによっては、連続してメールが送信された場合に、スパムメールと判定されることがある。
左記への対策としては、同一ドメインに対し連続で送信処理を行わないように、送信順序をランダムにする方法が考えられる。
|
.. TODO GreenMailはまだalpha版(2.1.0-alpha4)しか提供されておらず、FY2023でも案内しない方が良いためコメントアウトとする
https://greenmail-mail-test.github.io/greenmail/ によると、alpha版は「Please note that 'alpha' refers to possible changing API but not stability」と言っており安定していない。
なお、安定板としては2.0.1がリリースされているが、こちらはAngus Mailを使用していない。
.. _email-test-with-greenmail:
GreenMailを利用したテスト
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| メール送信機能をテストするためにフェイクサーバとして\ :url_greenmail:`GreenMail <>`\ を利用する方法を紹介する。
| GreenMailはライブラリとして利用する以外に、warファイルをデプロイして利用することも可能である。
|
| GreenMailを利用したテストコードの実装例を以下に示す。
\ **pom.xmlの設定例**\
.. code-block:: xml
com.icegreen
greenmail-spring
test
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | GreenMailのライブラリをdependenciesに追加する。
\ **JUnitソースの実装例**\
.. code-block:: java
@Inject
EmailService emailService;
@Rule
public final GreenMailRule greenMail = new GreenMailRule(
ServerSetupTest.SMTP); // (1)
@Test
public void testSend() {
String from = "info@example.com";
String to = "foo@example.com";
String subject = "Registration confirmation.";
String text = "Hi "
+ to
+ ", welcome to EXAMPLE.COM!\r\n"
+ "If you were not an intended recipient, Please notify the sender.";
emailService.send(from, to, subject, text);
assertTrue(greenMail.waitForIncomingEmail(3000, 1)); // (2)
Message[] messages = greenMail.getReceivedMessages(); // (3)
assertNotNull(messages);
assertEquals(1, messages.length);
// omitted
}
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | \ ``ServerSetupTest.SMTP``\ を指定した\ ``GreenMailRule``\ をルールとして設定する。
| SMTPのポート番号はデフォルトで\ ``3025``\ が使用される。
* - | (2)
- | \ ``waitForIncomingEmail``\ メソッドを利用してメールの到達を待機する。
| 別スレッドで非同期にメール送信が行われる際に利用する。
| この例では、メール送信が非同期で行われている前提で、1通のメールが到達するまで最大3秒待機する。
* - | (3)
- | \ ``getReceivedMessages``\ メソッドを利用してすべての受信メールを取得する。
| GreenMailで送信したメールは宛先に係らず、すべてGreenMailで受信される。
.. raw:: latex
\newpage