タイプセーフメール送信 (MailFlute)

LastaFluteの特徴の一つです。

(いま見ている)このページでは、FreeGenを使った大まかな実装の流れと、LastaFluteにおけるメールの環境設定などについてのみ記載していきます。 MailFlute自体の使い方(メールテンプレートの書き方など)は、MailFluteのオフィシャルドキュメントをご覧ください。

タイプセーフにメールとは?

MailFlute と FreeGen

LastaFluteでは、MailFlute と FreeGen を組み合わせて、タイプセーフにメールを送信します。

MailFluteは、メールテンプレート機能を備えたシンプルなメール送信ライブラリ、FreeGenは、DBFluteが提供する様々なケースで利用できる自動生成エンジン、これらを組み合わせてタイプセーフメールを実現します。

MailFlute Architecture

メールの実装の流れ

dfmailを作成

まず、src/main/resources/mail 配下に、dfmailのメールテンプレートを書きます。 外だしSQLに非常によく似たパラメータコメント形式 (pmfile形式) で書きます。

e.g. 新しい会員の登録を想定したメールテンプレート @welcome_member.dfmail
/*
 [New Member's Registration]
 The member will be formalized after click.
*/
subject: Welcome to your sign up, /*pmb.memberName*/
>>>
Hello, /*pmb.memberName*/

How are you?
/*IF pmb.birthdate != null*/
Wonderful birthdate! /*pmb.birthdate*/
/*END*/

Delivery Address: /*pmb.address:orElse('none')*/

Thanks

mailディレクトリ配下に自由にディレクトリを作ってOKです。何種類ものメールを飛ばすのであれば、業務カテゴリごとにディレクトリを作って分けて管理をすると良いでしょう。

FreeGenを叩いて自動生成

そして、FreeGenを叩き... (lastafluteMap.dfprop に mail が設定されていることが前提)

e.g. Manageで 12 (freegen) を叩く @Command
...$ sh manage.sh 12

mylasta.mail パッケージ配下に Postcard クラスが自動生成されます。 クラス名は、テンプレートファイルをキャメルケースにしたものになります。(welcome_member.dfmail なら WelcomeMemberPostcard)

PostboxをDI

メールを送信するためのコンポーネント、Postbox を DI します。

e.g. Postbox を DI @Java
@Resouce
private Postbox postbox;

droppedInto() でメール送信

自動生成された Postcard の droppedInto() メソッドでメール送信します。 第二引数の postcard インスタンスに対して、メール送信の設定をしていきます。

e.g. Postcard でメール送信 @Java
WelcomeMemberPostcard.droppedInto(postbox, postcard -> {
    postcard.setFrom("from@example.com", LABELS_OFFICE_MAIL);
    postcard.addTo("to@example.com");
    postcard.setMemberName("sea");
    postcard.setBirthdate(birthdate);
    postcard.addReplyTo("replyto@example.com");
});

MailFluteの使い方は?

MailFluteのオフィシャルページ

MailFlute自体の詳しい使い方は、MailFluteのオフィシャルドキュメントをご覧ください。

このページはLastaFluteに関わるお話を

(いま見ている)このページでは、FreeGenを使った大まかな実装の流れと、LastaFluteにおけるメールの環境設定などについてのみ記載していきます。

lastafluteMap.dfpropの設定

Postcardを自動生成するために、lastafluteMap.dfprop の freeGenList に mail を追加する必要があります。 Exampleデフォルトとして初めから追加されているので、Exampleからスタートアップした場合はあまり意識することはないでしょう。

e.g. add mail to freeGenList @lastafluteMap.dfprop
; appMap = map:{
    ; harbor = map:{
        ; path = ..
        ; freeGenList = list:{ env ; ... ; mail ; template ; ... }
        ; propertiesHtmlList = list:{ env ; config ; label ; message }
    }
}

もうひとつ、プラグインインターフェースを付与するときに、この dfprop を修正します(後述)。

デフォルトメソッドで再利用

お決まりの設定をどうにかしたい

例えば、どのメールでもお決まりの From を設定するというような場合...

e.g. setFrom() plain @Java
postcard.setFrom(config.getMailAddressSupport(), HarborMessages.LABELS_MAIL_SUPPORT_PERSONAL);

一行だけとは言え書くのは少し面倒です。ただ、そこよりも、何箇所にもわたってメール送信の実装をしていくと、 どこかで間違った From を指定してしまう可能性もあります。アドレスとpersonalがズレてしまったりなど。

そういった場合、こういう風に再利用したいものです。

e.g. setFrom() plain @Java
postcard.setFromSupport(config); // address and personal are set

ですが、Postcardは自動生成クラスです。ジェネレーションギャップにもなっていないので、クラスを書き換えることはできません。 書き換えられたとしてもすべての Postcard のそのメソッドを定義しないといけません(現実的ではない)

すべてのPostcardに同じインターフェースを

そこで、すべてのPostcardに同じインターフェースを付与することができるようになっています。

lastafluteMap.dfprop にて、mailPluginInterface でインターフェースのFQCNを指定すると、自動生成される Postcard が implements するようになります。

e.g. add mail to freeGenList @lastafluteMap.dfprop
; appMap = map:{
    ; harbor = map:{
        ; path = ..
        ; freeGenList = list:{ env ; ... ; mail ; template ; ... }
        ; propertiesHtmlList = list:{ env ; config ; label ; message }
        ; mailPluginInterface = org.docksidestage.mylasta.mail.PluggedinHarborPostcard
    }
}

このインターフェースはアプリで用意します。例えば、以下のように実装すると、すべての Postcard が setFromSupport() を持つようになります。

e.g. setFromSupport() as default method of interface @Java
public interface PluggedinHarborPostcard {

    void setFrom(String from, String personnel);

    default void setFromSupport(FortressConfig config) {
        setFrom(config.getMailAddressSupport(), FortressMessages.LABELS_MAIL_SUPPORT_PERSONAL);
    }
}

このように、多くの Postcard でお決まりの設定処理がある場合、デフォルトメソッドを用意しておけば、ディベロッパーは補完で選ぶだけで設定を済ませることができます。 結果、変なズレによるバグというのを減らせることができます。また、変更時の一括修正もしやすくなります。

メールの環境設定

そもそもメールの環境設定をするためには?

[app]_env.properties

Exampleテンプレート構成であれば、すでに [app]_env.properties にて、メールの送信先を設定することができます。

e.g. [app]_env.properties メールの設定 @Java
# ----------------------------------------------------------
#                                                      Mail
#                                                     ------
# Does it send mock mail? (true: no send actually, logging only)
mail.send.mock = true

# SMTP server settings for main: host:port
mail.smtp.server.main.host.and.port = localhost:25

# The prefix of subject to show test environment or not
mail.subject.test.prefix = [Test]

# The common return path of all mail
mail.return.path = returnpath@docksidestage.org

DeliveryDepartmentでもっと細かく

sponsor.[App]MailDeliveryDepartmentCreator にて、さらに細かく挙動を調整することができます。 このクラスは、[App]FwAssistantDirector から参照され、AssistantDirector の Direction として登録されます。

クラスレベルでの拡張ポイントとなり、振舞いなどを自由自在に変更することができます。 実際に、Exampleデフォルトとして、件名の[Test]付与、非同期処理の有効化、ラベルの活用など、LastaFluteとしての振舞いを実現するための拡張が組み込まれています。

さらに挙動を変更したり処理を追加したりする場合は、しっかりコードを読んで拡張しましょう。

e.g. HarborMailDeliveryDepartmentCreator @Java
public SMailDeliveryDepartment create() {
    return new SMailDeliveryDepartment(createPostalParkingLot(), createPostalPersonnel());
}

protected SMailPostalParkingLot createPostalParkingLot() {
    final SMailPostalParkingLot parkingLot = new SMailPostalParkingLot();
    final SMailPostalMotorbike motorbike = new SMailPostalMotorbike();
    final String hostAndPort = config.getMailSmtpServerMainHostAndPort();
    final List<String> hostPortList = DfStringUtil.splitListTrimmed(hostAndPort, ":");
    motorbike.registerConnectionInfo(hostPortList.get(0), Integer.parseInt(hostPortList.get(1)));
    motorbike.registerReturnPath(config.getMailReturnPath());
    parkingLot.registerMotorbikeAsMain(motorbike);
    return parkingLot;
}

protected SMailPostalPersonnel createPostalPersonnel() {
    final SMailDogmaticPostalPersonnel personnel = createDogmaticPostalPersonnel();
    return config.isMailSendMock() ? personnel.asTraining() : personnel;
}

protected SMailDogmaticPostalPersonnel createDogmaticPostalPersonnel() { // #ext_point e.g. locale, database
    final String testPrefix = config.getMailSubjectTestPrefix();
    final AsyncManager asyncManager = getAsyncManager();
    final MessageManager messageManager = getMessageManager();
    return new SMailDogmaticPostalPersonnel() {

        // *if you need user locale switching or templates from database,
        // override createConventionReceptionist() (see the method for the details)

        @Override
        protected OptionalThing<SMailSubjectFilter> createSubjectFilter() {
            return OptionalThing.of((view, subject) -> !subject.startsWith(testPrefix) ? testPrefix + subject : subject);
        }

        @Override
        protected OptionalThing<SMailAsyncStrategy> createAsyncStrategy() {
            return OptionalThing.of(new SMailAsyncStrategy() {
                @Override
                public void async(CardView view, Runnable runnable) {
                    asyncRunnable(asyncManager, runnable);
                }

                @Override
                public boolean alwaysAsync(CardView view) {
                    return true; // as default of LastaFlute example 
                }
            });
        }

        @Override
        protected OptionalThing<SMailLabelStrategy> createLabelStrategy() {
            return OptionalThing.of((view, locale, label) -> resolveLabelIfNeeds(messageManager, locale, label));
        }
    };
}