Spring Boot でメール送信する Web アプリケーションを作る ( その14 )( Thymeleaf を利用して HTML メールを送信する )
概要
Spring Boot でメール送信する Web アプリケーションを作る ( その13 )( メール送信画面の作成7 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- Thymeleaf による HTML メール送信機能を実装します。
- 記述が長すぎてブログの途中で表示が切れてしまったため、2回に分けます。
- 以下の仕様で実装します。
- メール送信画面の「送信」ボタンの右側に「HTMLメール送信」ボタンを追加し、「HTMLメール送信」ボタンがクリックされたらHTMLメールを送信します。
- 「HTMLメール送信」ボタンがクリックされた時の URL は /mailsend/sendhtml にします。
- HTML なのでテンプレートエンジンは Velocity ではなく Thymeleaf を使用します。
- HTMLメールのテンプレートは Ink のものを使用します。
- 今回はテストを作成しながら実装します。
- まさか HTML メールまでレスポンシブ対応になっているとは驚きでした。。。
ソフトウェア一覧
参考にしたサイト
HTMLメール制作のコツや便利なサービスいろいろ http://www.webcreatorbox.com/webinfo/html-email/
HTMLメールのレスポンシブ化
http://nxpg.net/blog/?p=1119Ink - スマホ/タブレット対応のレスポンシブHTMLメールテンプレート
http://www.moongift.jp/2013/11/ink-%E3%82%B9%E3%83%9E%E3%83%9B%E3%82%BF%E3%83%96%E3%83%AC%E3%83%83%E3%83%88%E5%AF%BE%E5%BF%9C%E3%81%AE%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B7%E3%83%96html%E3%83%A1%E3%83%BC%E3%83%AB%E3%83%86/Ink: A Responsive Email Framework from ZURB
http://zurb.com/ink/- HTML メールのテンプレートです。レスポンシブ対応しています。
Rich HTML email in Spring with Thymeleaf
http://www.thymeleaf.org/doc/articles/springmail.html- Thymeleaf のチュートリアルです。Spring Framework + Thymeleaf の組み合わせで HTML メールを送信する方法が書かれています。
MailSenderAutoConfiguration.java
https://github.com/spring-projects/spring-boot/blob/master/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailSenderAutoConfiguration.java- spring-boot-starter-mail を入れた時の AutoConfiguration 用クラスです。
org.springframework.mail.javamail - Class MimeMessageHelper
http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/mail/javamail/MimeMessageHelper.html- MimeMessageHelper クラスの JavaDoc です。
Spring Boot Internationalization with Default Locale for Message Strings
http://codedevstuff.blogspot.jp/2015/05/spring-boot-internationalization-with.html- Spring Boot で現在の Locale を取得する方法を参考にしました。
手順
ブランチの作成
- IntelliJ IDEA で 1.0.x-send-htmlmail ブランチを作成します。
HTML メールのテンプレートファイルの作成
Ink のページの「Download Ink」ボタンをクリックし、ink-1.0.5.zip をダウンロードします。
ink-1.0.5.zip を解凍し、boilerplate.html を src/main/resources/templates/mail/MAIL001 の下にコピーし、ファイル名を MAIL001-HTML-body.html に変更します。
src/main/resources/templates/mail/MAIL001 の下の MAIL001-HTML-body.html を リンク先の内容 に変更します。以下のレイアウトです。
MAIL001MailHelper クラスの変更
Thymeleaf の Rich HTML email in Spring with Thymeleaf のチュートリアルを見ると、HTML メールを送信するためのポイントは以下の通りです。
- SimpleMailMessage クラスではなく MimeMessage クラスを使用します。
- MimeMessage クラスのインスタンスを生成するためには JavaMailSender クラスの createMimeMessage メソッドを呼び出します。
- メールを生成する時には MimeMessage クラスのメソッドを直接呼び出すのではなく、Spring Framework の MimeMessageHelper クラスを利用します。
- メール本文をセットするために MimeMessageHelper クラスの setText メソッドを呼び出す時に、第2引数に true を指定します。第2引数は HTML メールか否かを示すフラグです。
JavaMailSender クラスはこれまでの実装で出てきていませんでした。どうすれば利用できるようになるのか調査します。
- 以前メール送信のために EmailService クラスに記述したのは
private MailSender mailSender;
であり、JavaMailSender ではありませんでした。 - spring-boot-starter-mail 用の AutoConfiguration 用クラスを Spring Boot の GitHub で確認すると MailSenderAutoConfiguration.java でした。
- MailSenderAutoConfiguration.java の mailSender Bean の戻り値を見ると JavaMailSenderImpl と書かれていました。Bean は JavaMailSenderImpl クラスのインスタンスを生成していましたので、@Autowired する側で MailSender ではなく JavaMailSender と定義すれば問題なく利用できるようです。
src/main/java/ksbysample/webapp/email/helper/mail の下の MAIL001MailHelper.java を リンク先の内容 に変更します。
MAIL001MailHelper クラスの generateTextUsingThymeleaf メソッドで Thymeleaf で HTML メールの本文を生成していますが、ポイントは以下の通りです。
- テンプレートエンジンに渡す値は org.thymeleaf.context.Context クラスのインスタンスを生成して setVariable メソッドでセットします。インスタンス生成時に Locale が必要ですが、LocaleContextHolder.getLocale() で取得します。
- Thymeleaf をテンプレートエンジンとして使用するために SpringTemplateEngine Bean を @Autowired アノテーションでインジェクションします。SpringTemplateEngine Bean は Spring Boot により自動で生成されますので、別途定義は不要です。
- SpringTemplateEngine クラスの process メソッドを呼び出して Thymeleaf のテンプレートファイルから HTML メールの本文を生成します。
テストを作成します。src/test/java/ksbysample/webapp/email/helper/mail の下の MAIL001MailHelperTest.java を リンク先の内容 に変更します。
テストを実行します。MAIL001MailHelperTest クラスのクラス名にカーソルを移動した後、コンテキストメニューを表示して「Run 'MAIL001MailHelperTes...' with Coverage」メニューを選択します。
テストが全て成功することを確認します。
EmailService クラスの変更
src/main/java/ksbysample/webapp/email/service の下の EmailService.java を リンク先の内容 に変更します。
テストを作成します。src/test/java/ksbysample/webapp/email/service の下の EmailServiceTest.java を リンク先の内容 に変更します。
テストを実行します。EmailServiceTest クラス内の「MailServerResourceを利用してメール送信する場合」クラスの testSendMail メソッドにカーソルを移動した後、コンテキストメニューを表示して「Run 'testSendMail()' with Coverage」メニューを選択します。
テストが全て成功することを確認します。
MailsendService クラスの変更
src/main/java/ksbysample/webapp/email/web/mailsend の下の MailsendService.java を リンク先の内容 に変更します。
テストを作成します。src/test/java/ksbysample/webapp/web/mailsend の下の MailsendServiceTest.java を リンク先の内容 に変更します。
テストを実行します。MailsendServiceTest クラスのクラス名にカーソルを移動した後、コンテキストメニューを表示して「Run 'MailsendServiceTest' with Coverage」メニューを選択します。
テストが全て成功することを確認します。
次回は。。。
この続きです。commit 等も次回実施します。
ソースコード
MAIL001-HTML-body.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta name="viewport" content="width=device-width"/> <style type="text/css"> /********************************************** * Ink v1.0.5 - Copyright 2013 ZURB Inc * **********************************************/ #outlook a { padding:0; } body{ width:100% !important; min-width: 100%; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0; } .ExternalClass { width:100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } #backgroundTable { margin:0; padding:0; width:100% !important; line-height: 100% !important; } img { outline:none; text-decoration:none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; } center { width: 100%; min-width: 580px; } a img { border: none; } p { margin: 0 0 0 10px; } table { border-spacing: 0; border-collapse: collapse; } td { word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !important; } table, tr, td { padding: 0; vertical-align: top; text-align: left; } hr { color: #d9d9d9; background-color: #d9d9d9; height: 1px; border: none; } /* Responsive Grid */ table.body { height: 100%; width: 100%; } table.container { width: 580px; margin: 0 auto; text-align: inherit; } table.row { padding: 0px; width: 100%; position: relative; } table.container table.row { display: block; } td.wrapper { padding: 10px 20px 0px 0px; position: relative; } table.columns, table.column { margin: 0 auto; } table.columns td, table.column td { padding: 0px 0px 10px; } table.columns td.sub-columns, table.column td.sub-columns, table.columns td.sub-column, table.column td.sub-column { padding-right: 10px; } td.sub-column, td.sub-columns { min-width: 0px; } table.row td.last, table.container td.last { padding-right: 0px; } table.one { width: 30px; } table.two { width: 80px; } table.three { width: 130px; } table.four { width: 180px; } table.five { width: 230px; } table.six { width: 280px; } table.seven { width: 330px; } table.eight { width: 380px; } table.nine { width: 430px; } table.ten { width: 480px; } table.eleven { width: 530px; } table.twelve { width: 580px; } table.one center { min-width: 30px; } table.two center { min-width: 80px; } table.three center { min-width: 130px; } table.four center { min-width: 180px; } table.five center { min-width: 230px; } table.six center { min-width: 280px; } table.seven center { min-width: 330px; } table.eight center { min-width: 380px; } table.nine center { min-width: 430px; } table.ten center { min-width: 480px; } table.eleven center { min-width: 530px; } table.twelve center { min-width: 580px; } table.one .panel center { min-width: 10px; } table.two .panel center { min-width: 60px; } table.three .panel center { min-width: 110px; } table.four .panel center { min-width: 160px; } table.five .panel center { min-width: 210px; } table.six .panel center { min-width: 260px; } table.seven .panel center { min-width: 310px; } table.eight .panel center { min-width: 360px; } table.nine .panel center { min-width: 410px; } table.ten .panel center { min-width: 460px; } table.eleven .panel center { min-width: 510px; } table.twelve .panel center { min-width: 560px; } .body .columns td.one, .body .column td.one { width: 8.333333%; } .body .columns td.two, .body .column td.two { width: 16.666666%; } .body .columns td.three, .body .column td.three { width: 25%; } .body .columns td.four, .body .column td.four { width: 33.333333%; } .body .columns td.five, .body .column td.five { width: 41.666666%; } .body .columns td.six, .body .column td.six { width: 50%; } .body .columns td.seven, .body .column td.seven { width: 58.333333%; } .body .columns td.eight, .body .column td.eight { width: 66.666666%; } .body .columns td.nine, .body .column td.nine { width: 75%; } .body .columns td.ten, .body .column td.ten { width: 83.333333%; } .body .columns td.eleven, .body .column td.eleven { width: 91.666666%; } .body .columns td.twelve, .body .column td.twelve { width: 100%; } td.offset-by-one { padding-left: 50px; } td.offset-by-two { padding-left: 100px; } td.offset-by-three { padding-left: 150px; } td.offset-by-four { padding-left: 200px; } td.offset-by-five { padding-left: 250px; } td.offset-by-six { padding-left: 300px; } td.offset-by-seven { padding-left: 350px; } td.offset-by-eight { padding-left: 400px; } td.offset-by-nine { padding-left: 450px; } td.offset-by-ten { padding-left: 500px; } td.offset-by-eleven { padding-left: 550px; } td.expander { visibility: hidden; width: 0px; padding: 0 !important; } table.columns .text-pad, table.column .text-pad { padding-left: 10px; padding-right: 10px; } table.columns .left-text-pad, table.columns .text-pad-left, table.column .left-text-pad, table.column .text-pad-left { padding-left: 10px; } table.columns .right-text-pad, table.columns .text-pad-right, table.column .right-text-pad, table.column .text-pad-right { padding-right: 10px; } /* Block Grid */ .block-grid { width: 100%; max-width: 580px; } .block-grid td { display: inline-block; padding:10px; } .two-up td { width:270px; } .three-up td { width:173px; } .four-up td { width:125px; } .five-up td { width:96px; } .six-up td { width:76px; } .seven-up td { width:62px; } .eight-up td { width:52px; } table.center, td.center { text-align: center; } h1.center, h2.center, h3.center, h4.center, h5.center, h6.center { text-align: center; } span.center { display: block; width: 100%; text-align: center; } img.center { margin: 0 auto; float: none; } .show-for-small, .hide-for-desktop { display: none; } /* Typography */ body, table.body, h1, h2, h3, h4, h5, h6, p, td { color: #222222; font-family: Verdana, Roboto, 'Droid Sans', 'メイリオ', Meiryo, 'MS Pゴシック', 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic; font-weight: normal; padding:0; margin: 0; text-align: left; line-height: 1.3; } h1, h2, h3, h4, h5, h6 { word-break: normal; } h1 {font-size: 40px;} h2 {font-size: 36px;} h3 {font-size: 32px;} h4 {font-size: 28px;} h5 {font-size: 24px;} h6 {font-size: 20px;} body, table.body, p, td {font-size: 14px;line-height:19px;} p.lead, p.lede, p.leed { font-size: 18px; line-height:21px; } p { margin-bottom: 10px; } small { font-size: 10px; } a { color: #2ba6cb; text-decoration: none; } a:hover { color: #2795b6 !important; } a:active { color: #2795b6 !important; } a:visited { color: #2ba6cb !important; } h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: #2ba6cb; } h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active { color: #2ba6cb !important; } h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited { color: #2ba6cb !important; } /* Panels */ .panel { background: #f2f2f2; border: 1px solid #d9d9d9; padding: 10px !important; } .sub-grid table { width: 100%; } .sub-grid td.sub-columns { padding-bottom: 0; } /* Buttons */ table.button, table.tiny-button, table.small-button, table.medium-button, table.large-button { width: 100%; overflow: hidden; } table.button td, table.tiny-button td, table.small-button td, table.medium-button td, table.large-button td { display: block; width: auto !important; text-align: center; background: #2ba6cb; border: 1px solid #2284a1; color: #ffffff; padding: 8px 0; } table.tiny-button td { padding: 5px 0 4px; } table.small-button td { padding: 8px 0 7px; } table.medium-button td { padding: 12px 0 10px; } table.large-button td { padding: 21px 0 18px; } table.button td a, table.tiny-button td a, table.small-button td a, table.medium-button td a, table.large-button td a { font-weight: bold; text-decoration: none; font-family: Helvetica, Arial, sans-serif; color: #ffffff; font-size: 16px; } table.tiny-button td a { font-size: 12px; font-weight: normal; } table.small-button td a { font-size: 16px; } table.medium-button td a { font-size: 20px; } table.large-button td a { font-size: 24px; } table.button:hover td, table.button:visited td, table.button:active td { background: #2795b6 !important; } table.button:hover td a, table.button:visited td a, table.button:active td a { color: #fff !important; } table.button:hover td, table.tiny-button:hover td, table.small-button:hover td, table.medium-button:hover td, table.large-button:hover td { background: #2795b6 !important; } table.button:hover td a, table.button:active td a, table.button td a:visited, table.tiny-button:hover td a, table.tiny-button:active td a, table.tiny-button td a:visited, table.small-button:hover td a, table.small-button:active td a, table.small-button td a:visited, table.medium-button:hover td a, table.medium-button:active td a, table.medium-button td a:visited, table.large-button:hover td a, table.large-button:active td a, table.large-button td a:visited { color: #ffffff !important; } table.secondary td { background: #e9e9e9; border-color: #d0d0d0; color: #555; } table.secondary td a { color: #555; } table.secondary:hover td { background: #d0d0d0 !important; color: #555; } table.secondary:hover td a, table.secondary td a:visited, table.secondary:active td a { color: #555 !important; } table.success td { background: #5da423; border-color: #457a1a; } table.success:hover td { background: #457a1a !important; } table.alert td { background: #c60f13; border-color: #970b0e; } table.alert:hover td { background: #970b0e !important; } table.radius td { -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; } table.round td { -webkit-border-radius: 500px; -moz-border-radius: 500px; border-radius: 500px; } /* Outlook First */ body.outlook p { display: inline !important; } /* Media Queries */ @media only screen and (max-width: 600px) { table[class="body"] img { width: auto !important; height: auto !important; } table[class="body"] center { min-width: 0 !important; } table[class="body"] .container { width: 95% !important; } table[class="body"] .row { width: 100% !important; display: block !important; } table[class="body"] .wrapper { display: block !important; padding-right: 0 !important; } table[class="body"] .columns, table[class="body"] .column { table-layout: fixed !important; float: none !important; width: 100% !important; padding-right: 0px !important; padding-left: 0px !important; display: block !important; } table[class="body"] .wrapper.first .columns, table[class="body"] .wrapper.first .column { display: table !important; } table[class="body"] table.columns td, table[class="body"] table.column td { width: 100% !important; } table[class="body"] .columns td.one, table[class="body"] .column td.one { width: 8.333333% !important; } table[class="body"] .columns td.two, table[class="body"] .column td.two { width: 16.666666% !important; } table[class="body"] .columns td.three, table[class="body"] .column td.three { width: 25% !important; } table[class="body"] .columns td.four, table[class="body"] .column td.four { width: 33.333333% !important; } table[class="body"] .columns td.five, table[class="body"] .column td.five { width: 41.666666% !important; } table[class="body"] .columns td.six, table[class="body"] .column td.six { width: 50% !important; } table[class="body"] .columns td.seven, table[class="body"] .column td.seven { width: 58.333333% !important; } table[class="body"] .columns td.eight, table[class="body"] .column td.eight { width: 66.666666% !important; } table[class="body"] .columns td.nine, table[class="body"] .column td.nine { width: 75% !important; } table[class="body"] .columns td.ten, table[class="body"] .column td.ten { width: 83.333333% !important; } table[class="body"] .columns td.eleven, table[class="body"] .column td.eleven { width: 91.666666% !important; } table[class="body"] .columns td.twelve, table[class="body"] .column td.twelve { width: 100% !important; } table[class="body"] td.offset-by-one, table[class="body"] td.offset-by-two, table[class="body"] td.offset-by-three, table[class="body"] td.offset-by-four, table[class="body"] td.offset-by-five, table[class="body"] td.offset-by-six, table[class="body"] td.offset-by-seven, table[class="body"] td.offset-by-eight, table[class="body"] td.offset-by-nine, table[class="body"] td.offset-by-ten, table[class="body"] td.offset-by-eleven { padding-left: 0 !important; } table[class="body"] table.columns td.expander { width: 1px !important; } table[class="body"] .right-text-pad, table[class="body"] .text-pad-right { padding-left: 10px !important; } table[class="body"] .left-text-pad, table[class="body"] .text-pad-left { padding-right: 10px !important; } table[class="body"] .hide-for-small, table[class="body"] .show-for-desktop { display: none !important; } table[class="body"] .show-for-small, table[class="body"] .hide-for-desktop { display: inherit !important; } </style> <style type="text/css"> .item-label { color: #0000FF; font-weight: bold; } </style> </head> <body> <table class="body"> <tr> <td class="center" align="center" valign="top"> <center> <table class="container"> <tr> <td> <table class="row"> <tr> <td class="wrapper last"> <table class="twelve columns"> <tr> <td> <h1>メール送信画面からのHTMLメール</h1> <p class="lead">画面から以下のデータが入力されました。</p> </td> <td class="expander"></td> </tr> </table> </td> </tr> </table> <table class="row"> <tr> <td class="wrapper"> <table class="two columns"> <tr> <td class="item-label">氏名</td> <td class="expander"></td> </tr> </table> </td> <td class="wrapper last"> <table class="ten columns"> <tr> <td id="name" th:text="${name}">田中 太郎</td> <td class="expander"></td> </tr> </table> </td> </tr> </table> <table class="row"> <tr> <td class="wrapper"> <table class="two columns"> <tr> <td class="item-label">性別</td> <td class="expander"></td> </tr> </table> </td> <td class="wrapper last"> <table class="ten columns"> <tr> <td id="sex" th:text="${sex}">男性</td> <td class="expander"></td> </tr> </table> </td> </tr> </table> <table class="row"> <tr> <td class="wrapper"> <table class="two columns"> <tr> <td class="item-label">項目</td> <td class="expander"></td> </tr> </table> </td> <td class="wrapper last"> <table class="ten columns"> <tr> <td id="type" th:text="${type}">資料請求</td> <td class="expander"></td> </tr> </table> </td> </tr> </table> <table class="row"> <tr> <td class="wrapper"> <table class="two columns"> <tr> <td class="item-label">商品</td> <td class="expander"></td> </tr> </table> </td> <td class="wrapper last"> <table class="ten columns"> <tr> <td id="item" th:text="${item}">商品1, 商品2, 商品3</td> <td class="expander"></td> </tr> </table> </td> </tr> </table> <hr/> <table class="row"> <tr> <td class="wrapper last"> <table class="twelve columns"> <tr> <td class="item-label">内容</td> <td class="expander"></td> </tr> </table> </td> </tr> </table> <table class="row"> <tr> <td class="wrapper last"> <table class="twelve columns"> <tr> <td id="naiyo" th:text="${naiyo}">これはテストです。</td> <td class="expander"></td> </tr> </table> </td> </tr> </table> <!-- container end below --> </td> </tr> </table> </center> </td> </tr> </table> </body> </html>
- 先頭の DOCTYPE は
<!DOCTYPE html>
に変更します。org.w3c.dom.Document クラスでメール本文のチェックをする際に時間がかからないようにするためです。詳細は Spring Boot + Thymeleaf の Web アプリを MockMvc でテストした時に遅かった理由とは? 参照。 - html タグに
xmlns:th="http://www.thymeleaf.org"
を追加します。 <link rel="stylesheet" href="ink.css"> <!-- For testing only -->
は削除します。- 最初の
<style type="text/css"> /* Ink styles go here in production */ </style>
の中に ink.css の中身をコピー&ペーストします。ただし/* Client-specific Styles & Reset */
と/* Alignment & Visibility Classes */
はorg.xml.sax.SAXParseException: エンティティ参照では、エンティティ名は'&'の直後に指定する必要があります。
のエラーが出るため削除します。 - Form クラスのデータを出力するところには
id="name"
のように id 属性を追加します。テストで値が出力されているかチェックする時に XPath で指定しやすくするためです。
MAIL001MailHelper.java
package ksbysample.webapp.email.helper.mail; import ksbysample.webapp.email.config.Constant; import ksbysample.webapp.email.util.VelocityUtils; import ksbysample.webapp.email.web.mailsend.MailsendForm; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Component; import org.thymeleaf.context.Context; import org.thymeleaf.spring4.SpringTemplateEngine; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; @Component public class MAIL001MailHelper { private final String TEMPLATE_LOCATION_TEXTMAIL = "mail/MAIL001/MAIL001-body.vm"; // templateEngine.processに渡すThymeleafのテンプレートファイルは拡張子.html付けないこと private final String TEMPLATE_LOCATION_HTMLMAIL = "mail/MAIL001/MAIL001-HTML-body"; @Autowired private VelocityUtils velocityUtils; @Autowired private JavaMailSender mailSender; @Autowired private SpringTemplateEngine templateEngine; public SimpleMailMessage createMessage(MailsendForm mailsendForm) { SimpleMailMessage mailMessage = new SimpleMailMessage(); mailMessage.setFrom(mailsendForm.getFromAddr()); mailMessage.setTo(mailsendForm.getToAddr()); mailMessage.setSubject(mailsendForm.getSubject()); mailMessage.setText(generateTextUsingVelocity(mailsendForm)); return mailMessage; } public MimeMessage createHtmlMessage(MailsendForm mailsendForm) throws MessagingException { MimeMessage mimeMessage = this.mailSender.createMimeMessage(); MimeMessageHelper message = new MimeMessageHelper(mimeMessage, false, "UTF-8"); message.setFrom(mailsendForm.getFromAddr()); message.setTo(mailsendForm.getToAddr()); message.setSubject(mailsendForm.getSubject()); message.setText(generateTextUsingThymeleaf(mailsendForm), true); return message.getMimeMessage(); } private String generateTextUsingVelocity(MailsendForm mailsendForm) { Constant constant = Constant.getInstance(); Map<String, Object> model = new HashMap<>(); model.put("name", mailsendForm.getName()); model.put("sex", constant.SEX_MAP.get(mailsendForm.getSex())); model.put("type", constant.TYPE_MAP.get(mailsendForm.getType())); String itemList = null; if (mailsendForm.getItem() != null) { itemList = mailsendForm.getItem().stream() .map(constant.ITEM_MAP::get) .collect(Collectors.joining(", ")); } model.put("item", itemList); model.put("naiyo", mailsendForm.getNaiyo()); return velocityUtils.merge(this.TEMPLATE_LOCATION_TEXTMAIL, model); } private String generateTextUsingThymeleaf(MailsendForm mailsendForm) { Constant constant = Constant.getInstance(); Context ctx = new Context(LocaleContextHolder.getLocale()); ctx.setVariable("name", mailsendForm.getName()); ctx.setVariable("sex", constant.SEX_MAP.get(mailsendForm.getSex())); ctx.setVariable("type", constant.TYPE_MAP.get(mailsendForm.getType())); String itemList = null; if (mailsendForm.getItem() != null) { itemList = mailsendForm.getItem().stream() .map(constant.ITEM_MAP::get) .collect(Collectors.joining(", ")); } ctx.setVariable("item", itemList); ctx.setVariable("naiyo", mailsendForm.getNaiyo()); return this.templateEngine.process(this.TEMPLATE_LOCATION_HTMLMAIL, ctx); } }
- テンプレートファイルのロケーションの定数を
templateLocation
→TEMPLATE_LOCATION_TEXTMAIL
へ変更します。定数なのに大文字、スネークケースで宣言していなかったので変更しました。 private final String TEMPLATE_LOCATION_HTMLMAIL = "mail/MAIL001/MAIL001-HTML-body";
を追加します。private JavaMailSender mailSender;
を追加します。private SpringTemplateEngine templateEngine;
を追加します。- createHtmlMessage メソッドを追加します。
- generateTextUsingThymeleaf メソッドを追加します。
MAIL001MailHelperTest.java
package ksbysample.webapp.email.helper.mail; import ksbysample.webapp.email.Application; import ksbysample.webapp.email.config.Constant; import ksbysample.webapp.email.web.mailsend.MailsendForm; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.mail.SimpleMailMessage; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.yaml.snakeyaml.Yaml; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import javax.xml.parsers.DocumentBuilderFactory; import java.io.StringReader; import java.util.stream.Collectors; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; @RunWith(Enclosed.class) public class MAIL001MailHelperTest { @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class テキストメール生成のテスト { private final MailsendForm mailsendFormSimple = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("/ksbysample/webapp/email/web/mailsend/mailsendForm_simple.yml")); private final MailsendForm mailsendFormMinimum = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("/ksbysample/webapp/email/web/mailsend/mailsendForm_minimum.yml")); @Autowired private MAIL001MailHelper mail001MailHelper; @Test public void MailsendFormの全てに値がセットされている場合() throws Exception { SimpleMailMessage message = mail001MailHelper.createMessage(mailsendFormSimple); assertThat(message.getFrom(), is(mailsendFormSimple.getFromAddr())); } @Test public void MailsendFormの必須項目のみ値がセットされている場合() throws Exception { SimpleMailMessage message = mail001MailHelper.createMessage(mailsendFormMinimum); assertThat(message.getFrom(), is(mailsendFormMinimum.getFromAddr())); } } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class HTMLメール生成のテスト { private final MailsendForm mailsendFormSimple = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("/ksbysample/webapp/email/web/mailsend/mailsendForm_simple.yml")); private final MailsendForm mailsendFormMinimum = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("/ksbysample/webapp/email/web/mailsend/mailsendForm_minimum.yml")); @Autowired private MAIL001MailHelper mail001MailHelper; @Test public void MailsendFormの全てに値がセットされている場合() throws Exception { MimeMessage message = mail001MailHelper.createHtmlMessage(mailsendFormSimple); // from, Subject InternetAddress address = (InternetAddress) message.getFrom()[0]; assertThat(address.getAddress(), is(mailsendFormSimple.getFromAddr())); assertThat(message.getSubject(), is(mailsendFormSimple.getSubject())); // メール本文 Document document = DocumentBuilderFactory .newInstance() .newDocumentBuilder() .parse(new InputSource(new StringReader((String) message.getContent()))); Constant constant = Constant.getInstance(); assertThat(document, hasXPath("//*[@id=\"name\"]", equalTo(mailsendFormSimple.getName()))); assertThat(document, hasXPath("//*[@id=\"sex\"]", equalTo(constant.SEX_MAP.get(mailsendFormSimple.getSex())))); assertThat(document, hasXPath("//*[@id=\"type\"]", equalTo(constant.TYPE_MAP.get(mailsendFormSimple.getType())))); assertThat(document, hasXPath("//*[@id=\"item\"]" , equalTo( mailsendFormSimple.getItem().stream() .map(constant.ITEM_MAP::get) .collect(Collectors.joining(", "))))); assertThat(document, hasXPath("//*[@id=\"naiyo\"]", equalTo(mailsendFormSimple.getNaiyo()))); } @Test public void MailsendFormの必須項目のみ値がセットされている場合() throws Exception { MimeMessage message = mail001MailHelper.createHtmlMessage(mailsendFormMinimum); // from, Subject InternetAddress address = (InternetAddress) message.getFrom()[0]; assertThat(address.getAddress(), is(mailsendFormMinimum.getFromAddr())); assertThat(message.getSubject(), is(mailsendFormMinimum.getSubject())); // メール本文 Document document = DocumentBuilderFactory .newInstance() .newDocumentBuilder() .parse(new InputSource(new StringReader((String) message.getContent()))); Constant constant = Constant.getInstance(); assertThat(document, hasXPath("//*[@id=\"name\"]", equalTo(""))); assertThat(document, hasXPath("//*[@id=\"sex\"]", equalTo(""))); assertThat(document, hasXPath("//*[@id=\"type\"]", equalTo(""))); assertThat(document, hasXPath("//*[@id=\"item\"]", equalTo(""))); assertThat(document, hasXPath("//*[@id=\"naiyo\"]", equalTo(""))); } } }
- ネストしたクラスで構造化し、既存のテストを「テキストメール生成のテスト」クラスに入れて、「HTMLメール生成のテスト」クラスを追加します。
- MAIL001MailHelperTest クラスには
@RunWith(Enclosed.class)
を追加します。 - 「HTMLメール生成のテスト」クラスに「テキストメール生成のテスト」クラスと同じテストメソッドを定義し、実装します。
- 「HTMLメール生成のテスト」クラスのテストメソッドではメール本文のチェックを追加します。org.w3c.dom.Document クラスで HTML メールの本文を解析した後、
assertThat(document, hasXPath("...", equalTo("...")));
でチェックします。
EmailService.java
package ksbysample.webapp.email.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; import javax.mail.internet.MimeMessage; @Service public class EmailService { @Autowired private JavaMailSender mailSender; public void sendSimpleMail(SimpleMailMessage mailMessage) { mailSender.send(mailMessage); } public void sendMail(MimeMessage message) { mailSender.send(message); } }
private MailSender mailSender;
→private JavaMailSender mailSender;
へ変更します。MailSender のままだと send メソッドで MimeMessage を送信できませんでした。- sendMail メソッドを追加します。
EmailServiceTest.java
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class MailServerResourceを利用してメール送信する場合 { @Rule @Autowired public MailServerResource mailServer; @Autowired private JavaMailSender mailSender; @Autowired private EmailService emailService; @Test public void testSendSimpleMail() throws Exception { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom("test@sample.com"); message.setTo("xxx@yyy.zzz"); message.setSubject("テスト"); message.setText("これはテストです"); emailService.sendSimpleMail(message); assertThat(mailServer.getMessagesCount(), is(1)); MimeMessage receiveMessage = mailServer.getFirstMessage(); assertThat(receiveMessage.getFrom()[0], is(new InternetAddress("test@sample.com"))); assertThat(receiveMessage.getAllRecipients()[0], is(new InternetAddress("xxx@yyy.zzz"))); assertThat(receiveMessage.getSubject(), is("テスト")); assertThat(receiveMessage.getContent(), is("これはテストです")); } @Test public void testSendMail() throws Exception { MimeMessage mimeMessage = mailSender.createMimeMessage(); MimeMessageHelper message = new MimeMessageHelper(mimeMessage); message.setFrom("test@sample.com"); message.setTo("xxx@yyy.zzz"); message.setSubject("テスト"); message.setText("これはテストです"); emailService.sendMail(message.getMimeMessage()); assertThat(mailServer.getMessagesCount(), is(1)); MimeMessage receiveMessage = mailServer.getFirstMessage(); assertThat(receiveMessage.getFrom()[0], is(new InternetAddress("test@sample.com"))); assertThat(receiveMessage.getAllRecipients()[0], is(new InternetAddress("xxx@yyy.zzz"))); assertThat(receiveMessage.getSubject(), is("テスト")); assertThat(receiveMessage.getContent(), is("これはテストです")); } }
- 「MailServerResourceを利用してメール送信する場合」クラスを変更します。
private JavaMailSender mailSender;
を追加します。- testSendMail メソッドを追加します。
MailsendService.java
package ksbysample.webapp.email.web.mailsend; import ksbysample.webapp.email.dao.EmailDao; import ksbysample.webapp.email.dao.EmailItemDao; import ksbysample.webapp.email.entity.Email; import ksbysample.webapp.email.entity.EmailItem; import ksbysample.webapp.email.helper.mail.MAIL001MailHelper; import ksbysample.webapp.email.service.EmailService; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.SimpleMailMessage; import org.springframework.stereotype.Service; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; @Service public class MailsendService { @Autowired private EmailDao emailDao; @Autowired private EmailItemDao emailItemDao; @Autowired private MAIL001MailHelper mail001MailHelper; @Autowired private EmailService emailService; public void saveAndSendEmail(MailsendForm mailsendForm) { // 入力されたデータを email, email_item テーブルに保存する saveEmail(mailsendForm); // メールを送信する sendEmail(mailsendForm); } public void saveAndSendHtmlEmail(MailsendForm mailsendForm) throws MessagingException { // 入力されたデータを email, email_item テーブルに保存する saveEmail(mailsendForm); // HTMLメールを送信する sendHtmlEmail(mailsendForm); } private void saveEmail(MailsendForm mailsendForm) { // email テーブルに保存する Email email = new Email(); BeanUtils.copyProperties(mailsendForm, email); emailDao.insert(email); // email_item テーブルに保存する EmailItem emailItem = new EmailItem(); if (mailsendForm.getItem() != null) { for (String item : mailsendForm.getItem()) { emailItem.setEmailItemId(null); emailItem.setEmailId(email.getEmailId()); emailItem.setItem(item); emailItemDao.insert(emailItem); } } } private void sendEmail(MailsendForm mailsendForm) { SimpleMailMessage message = mail001MailHelper.createMessage(mailsendForm); emailService.sendSimpleMail(message); } private void sendHtmlEmail(MailsendForm mailsendForm) throws MessagingException { MimeMessage message = mail001MailHelper.createHtmlMessage(mailsendForm); emailService.sendMail(message); } }
- saveAndSendHtmlEmail メソッドを追加します。
- sendHtmlEmail メソッドを追加します。
- saveEmail メソッド、sendEmail メソッドのアクセス修飾子を public → private に変更します。
- sendEmail メソッド内の変数名を sendHtmlEmail メソッドのものに合わせるために
mailMessage
→message
へ変更します。
MailsendServiceTest.java
package ksbysample.webapp.email.web.mailsend; import com.google.common.io.Files; import ksbysample.webapp.email.Application; import ksbysample.webapp.email.config.Constant; import ksbysample.webapp.email.test.MailServerResource; import ksbysample.webapp.email.test.TableDataAssert; import ksbysample.webapp.email.test.TestDataResource; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.csv.CsvDataSet; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.yaml.snakeyaml.Yaml; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import javax.sql.DataSource; import javax.xml.parsers.DocumentBuilderFactory; import java.io.File; import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class MailsendServiceTest { // MailsendFormの全てに値がセットされているテストデータ private final MailsendForm mailsendFormSimple = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_simple.yml")); // MailsendFormの必須項目のみ値がセットされているテストデータ private final MailsendForm mailsendFormMinimum = (MailsendForm) new Yaml().load(getClass().getResourceAsStream("mailsendForm_minimum.yml")); @Rule @Autowired public TestDataResource testDataResource; @Rule @Autowired public MailServerResource mailServer; @Autowired private DataSource dataSource; @Autowired private MailsendService mailsendService; @Test public void mailsendFormSimpleでテキストメールを送信する場合() throws Exception { mailsendService.saveAndSendEmail(mailsendFormSimple); // email, email_item テーブルに保存されているか確認する IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/email/web/mailsend/testdata/simple")); TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource); tableDataAssert.assertEquals("email", new String[]{"email_id"}); tableDataAssert.assertEquals("email_item", new String[]{"email_item_id", "email_id"}); // メールが送信されているか確認する assertThat(mailServer.getMessagesCount(), is(1)); MimeMessage receiveMessage = mailServer.getFirstMessage(); assertThat(receiveMessage.getFrom()[0], is(new InternetAddress("test@sample.com"))); assertThat(receiveMessage.getAllRecipients()[0], is(new InternetAddress("xxx@yyy.zzz"))); assertThat(receiveMessage.getSubject(), is("テスト")); String mailsendFormSimpleMail = Files.toString(new File(getClass().getResource("mailsendForm_simple_mail.txt").toURI()), StandardCharsets.UTF_8); assertThat(receiveMessage.getContent(), is(mailsendFormSimpleMail)); } @Test public void mailsendFormMinimumでテキストメールを送信する場合() throws Exception { mailsendService.saveAndSendEmail(mailsendFormMinimum); // email, email_item テーブルに保存されているか確認する IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/email/web/mailsend/testdata/minimum")); TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource); tableDataAssert.assertEquals("email", new String[]{"email_id"}); tableDataAssert.assertEquals("email_item", new String[]{"email_item_id", "email_id"}); // メールが送信されているか確認する assertThat(mailServer.getMessagesCount(), is(1)); MimeMessage receiveMessage = mailServer.getFirstMessage(); assertThat(receiveMessage.getFrom()[0], is(new InternetAddress("test@sample.com"))); assertThat(receiveMessage.getAllRecipients()[0], is(new InternetAddress("xxx@yyy.zzz"))); assertThat(receiveMessage.getSubject(), is("テスト")); String mailsendFormMinimumMail = Files.toString(new File(getClass().getResource("mailsendForm_minimum_mail.txt").toURI()), StandardCharsets.UTF_8); assertThat(receiveMessage.getContent(), is(mailsendFormMinimumMail)); } @Test public void mailsendFormSimpleでHTMLメールを送信する場合() throws Exception { mailsendService.saveAndSendHtmlEmail(mailsendFormSimple); // email, email_item テーブルに保存されているか確認する IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/email/web/mailsend/testdata/simple")); TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource); tableDataAssert.assertEquals("email", new String[]{"email_id"}); tableDataAssert.assertEquals("email_item", new String[]{"email_item_id", "email_id"}); // メールが送信されているか確認する assertThat(mailServer.getMessagesCount(), is(1)); MimeMessage receiveMessage = mailServer.getFirstMessage(); assertThat(receiveMessage.getFrom()[0], is(new InternetAddress("test@sample.com"))); assertThat(receiveMessage.getAllRecipients()[0], is(new InternetAddress("xxx@yyy.zzz"))); assertThat(receiveMessage.getSubject(), is("テスト")); // メール本文 Document document = DocumentBuilderFactory .newInstance() .newDocumentBuilder() .parse(new InputSource(new StringReader((String) receiveMessage.getContent()))); Constant constant = Constant.getInstance(); assertThat(document, hasXPath("//*[@id=\"name\"]", equalTo(mailsendFormSimple.getName()))); assertThat(document, hasXPath("//*[@id=\"sex\"]", equalTo(constant.SEX_MAP.get(mailsendFormSimple.getSex())))); assertThat(document, hasXPath("//*[@id=\"type\"]", equalTo(constant.TYPE_MAP.get(mailsendFormSimple.getType())))); assertThat(document, hasXPath("//*[@id=\"item\"]" , equalTo( mailsendFormSimple.getItem().stream() .map(constant.ITEM_MAP::get) .collect(Collectors.joining(", "))))); assertThat(document, hasXPath("//*[@id=\"naiyo\"]", equalTo(mailsendFormSimple.getNaiyo()))); } @Test public void mailsendFormMinimumでHTMLメールを送信する場合() throws Exception { mailsendService.saveAndSendHtmlEmail(mailsendFormMinimum); // email, email_item テーブルに保存されているか確認する IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/email/web/mailsend/testdata/minimum")); TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource); tableDataAssert.assertEquals("email", new String[]{"email_id"}); tableDataAssert.assertEquals("email_item", new String[]{"email_item_id", "email_id"}); // メールが送信されているか確認する assertThat(mailServer.getMessagesCount(), is(1)); MimeMessage receiveMessage = mailServer.getFirstMessage(); assertThat(receiveMessage.getFrom()[0], is(new InternetAddress("test@sample.com"))); assertThat(receiveMessage.getAllRecipients()[0], is(new InternetAddress("xxx@yyy.zzz"))); assertThat(receiveMessage.getSubject(), is("テスト")); // メール本文 Document document = DocumentBuilderFactory .newInstance() .newDocumentBuilder() .parse(new InputSource(new StringReader((String) receiveMessage.getContent()))); Constant constant = Constant.getInstance(); assertThat(document, hasXPath("//*[@id=\"name\"]", equalTo(""))); assertThat(document, hasXPath("//*[@id=\"sex\"]", equalTo(""))); assertThat(document, hasXPath("//*[@id=\"type\"]", equalTo(""))); assertThat(document, hasXPath("//*[@id=\"item\"]", equalTo(""))); assertThat(document, hasXPath("//*[@id=\"naiyo\"]", equalTo(""))); } }
- 既存のテストメソッド名を変更します。
MailsendFormの全てに値がセットされている場合
→mailsendFormSimpleでテキストメールを送信する場合
MailsendFormの必須項目のみ値がセットされている場合
→mailsendFormMinimumでテキストメールを送信する場合
- 「mailsendFormSimpleでHTMLメールを送信する場合」メソッドを追加します。
- 「mailsendFormMinimumでHTMLメールを送信する場合」メソッドを追加します。
- メールサーバ経由での HTML メールの送信結果を確認するために、メール本文のチェック処理は MAIL001MailHelperTest クラスで作成したものを持ってきました。同じ処理をコピーしているのですが、テストの場合も同じ処理ならばチェック処理としてどこかにまとめた方がよいのでしょうか? それとも同じチェックをしていること自体が何か間違っているのでしょうか? ちょっと悩みつつも今回はこのままで行きます。
履歴
2015/05/27
初版発行。