How to send an HTML email from a scheduler with Play 2.5?

Hi,

I have a scheduler to query periodically users inactivity and send an e-mail to inform them when their account will be disabled.

public class UserChecker
{
    private static final FiniteDuration INTERVAL = Duration.create(1, TimeUnit.DAYS);

    private ActorSystem actorSystem;
    private ExecutionContext executionContext;

    @Inject
    MailerClient mailerClient;

    @Inject
    Configuration configuration;

    @Inject
    public UserChecker(ActorSystem actorSystem, ExecutionContext executionContext)
    {
        this.actorSystem = actorSystem;
        this.executionContext = executionContext;

        this.initialize();
    }

    private void initialize()
    {
        actorSystem.scheduler().schedule(
            Duration.create(0, TimeUnit.SECONDS),
            INTERVAL,
            () -> check(),
            executionContext
        );
    }

    private void check()
    {        
        for (User user : User.findOutdatedPrepay())
        {
            Email email = new Email();
            email.setSubject("Subject");
            email.setFrom(Format.pretty(configuration.getString("coordinates.name"), configuration.getString("play.mailer.user")));
            email.addTo("test@me.com");
            email.setBodyHtml(prepayNotRefundedWarning.render(user, configuration).body());

            try
            {
                    mailerClient.send(email);
            }
            catch (Exception e)
            {
                    e.printStackTrace();
            }
        }
    }
}

I would like to use a view to render the HTML of my email but I get an error at the line where the view is rendered:

java.lang.RuntimeException: There is no HTTP Context available from here.

How should I do that?
Regards

Hi @cyrilfr,

At some point, that the stack trace will reveal, you are trying to access the current Http.Context. From a scheduler point of view, that won’t make sense since there is no current request available to the scheduler. It means the scheduler needs to have all the necessary data to run without relying on any request data.

Since I’m not seeing any access to Http.Context in the code you shared, it is maybe happening in prepayNotRefundedWarning view? Better if you can share the complete stack trace.

Best.

Hi @marcospereira,

Yes, there is reverse routing in the view.

Now I get another error: java.util.NoSuchElementException: None.get

@cyrilfr,

Yes, better to not use that since it depends on the request (to compose the absolute URL with scheme, host, etc.). Better to have this as a configuration that you can access and use the reverse router just to get the path section.

Now I get another error: java.util.NoSuchElementException: None.get

Hard to know what is happening based only on this line. Post the complete stack trace. But usually, you don’t do a get on an Option because it can be None. Better to use getOrElse if this is inside a view.

Best.

Here is the view:

@(user: User, configuration: play.Configuration)
@import helpers.Format

@amount = {
    font-size: 30px;
    font-weight: 300;
}

@style = {
    .amount { @amount }
}

@layouts.email(configuration, true, style) {
    <p>@Messages("prepay.head")</p>
    <table border="0" cellpadding="5" width="100%">
        <tr>
            <td align="right" valign="top" width="50%"><strong>@Messages("form.user") :</strong></td>
            <td align="left" valign="top">@user.getLabel()</td>
        </tr>
        <tr>
            <td align="right" valign="top" width="50%"><strong>@Messages("form.user.building") :</strong></td>
            <td align="left" valign="top">@user.getAddress1()<br>@user.getPostalCode() @user.getCity()</td>
        </tr>
        @if(user.getEmail() != null && !user.getEmail().isEmpty()) {
            <tr>
                <td align="right" valign="top" width="50%"><strong>@Messages("form.user.email") :</strong></td>
                <td align="left" valign="top">@user.getEmail()</td>
            </tr>
        }
        @if(user.getMobile() != null && !user.getMobile().isEmpty()) {
            <tr>
                <td align="right" valign="top"><strong>@Messages("form.user.mobile") :</strong></td>
                <td align="left" valign="top">@user.getMobile()</td>
            </tr>
        }
        <tr>
            <td align="right" valign="top"><strong>@Messages("form.user.lastRefill"):</strong></td>
            <td align="left" valign="top">@Format.date(Messages("format.dateTime"), user.getLastRefill())</td>
        </tr>
        <tr>
            <td align="right" valign="top"><strong>@Messages("form.user.credit") :</strong></td>
            <td align="left" valign="top"><div style="@amount" class="amount">@Messages("currency.swissFranc.short") @Format.price(user.getCredit())</div></td>
        </tr>
        <tr>
            <td align="right" valign="top"><strong>@Messages("form.user.prepay") :</strong></td>
            <td align="left" valign="top"><div style="@amount" class="amount">@Messages("currency.swissFranc.short") @Format.price(user.getPrepayAmount())</div></td>
        </tr>
        <tr>
            <td align="right" valign="top"><strong>@Messages("form.user.fees"):</strong></td>
            <td align="left" valign="top"><div style="@amount" class="amount">@Messages("currency.swissFranc.short") @Format.price(configuration.getInt("eeproperty.tax.prepayReminder"))</div></td>
        </tr>
        <tr>
            <td align="right" valign="top"><strong>@Messages("form.user.total") :</strong></td>
            <td align="left" valign="top"><div style="@amount" class="amount"><u>@Messages("currency.swissFranc.short") @Format.price(user.getPrepayAmount() + configuration.getInt("eeproperty.tax.prepayReminder"))</u></div></td>
        </tr>
    </table>
}

The None.get error message was related to a bad Configuration dependency injection. Now I have troubles with Messages: java.lang.IllegalArgumentException: Unknown pattern letter: f which mean that the “format.dateTime” label isn’t matched in the language file.
This label exists and works perfecly when the view is rendered in a controller.

My Format.date() method looks like:

public static String date(String format, LocalDateTime date)
{
    if (format == null || date == null) return "";
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
    return date.atZone(ZoneId.systemDefault()).format(formatter);
}

Hey @cyrilfr,

It is very hard to know what could be possibly happening when all we got is just a small section of the stack trace. Can you better detail what you are trying to do? How your project is configured? Where is this error happening? What is the complete stack trace?

Best.

What is the complete stack trace?

@marcospereira,

The complete stacktrace is:

java.lang.IllegalArgumentException: Unknown pattern letter: f
	at java.time.format.DateTimeFormatterBuilder.parsePattern(DateTimeFormatterBuilder.java:1661)
	at java.time.format.DateTimeFormatterBuilder.appendPattern(DateTimeFormatterBuilder.java:1570)
	at java.time.format.DateTimeFormatter.ofPattern(DateTimeFormatter.java:536)
	at helpers.Format.date(Format.java:37)
	at views.html.mail.prepayNotRefundedWarning_Scope0$prepayNotRefundedWarning.apply(prepayNotRefundedWarning.template.scala:83)
	at views.html.mail.prepayNotRefundedWarning_Scope0$prepayNotRefundedWarning.render(prepayNotRefundedWarning.template.scala:99)
	at views.html.mail.prepayNotRefundedWarning.render(prepayNotRefundedWarning.template.scala)
	at actors.UserChecker.check(UserChecker.java:82)
	at actors.UserChecker.lambda$initialize$0(UserChecker.java:57)
	at akka.actor.LightArrayRevolverScheduler$$anon$2$$anon$1.run(LightArrayRevolverScheduler.scala:102)
	at akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:39)
	at akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinTask.exec(AbstractDispatcher.scala:415)
	at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
	at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
	at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
	at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)

This line: Format.date(Messages("format.dateTime", user.getLastRefill()) should be displaying the date in the right format, depending on the language file. For example, in german it would be Format.date("dd.mm.YYYY HH:mm", user.getLastRefill()) and display 27.11.2017 13:30.
Instead it gets Format.date("format.dateTime", user.getLastRefill()) and f isn’t a valid DateTimeFormatter pattern.

I guess Messages() is doing something behind the scene to work with a controller action that is missing here.

Can you better detail what you are trying to do? How your project is configured? Where is this error happening?

I want to send e-mail automatically to specific users. The error happen when the e-mail is being sent.

And which format are you using? I mean, what is the value for format in Format.date? It looks like you are using an invalid format since lowercased f is not a defined for format patterns:

https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html

Best.

@marcospereira

The format normally returned is dd.MM.yyyy HH:mm:ss. In this case, the f is because Messages() doesn’t work.

@marcospereira

I guess that because the Http.Context is not available, Messages() cannot know what language to use for the translation. Is there a way to pass the language code to use (for ex the language of the user)?

How could I pass the language to use for the Messages API to make it work please?

If you are not particular about i18N, you can useLang.defaultLang() which defaults to jvm locale.

@aditya

I need to use the language selected by the user.

I did someting like that to pass the user language to the template:

public class UserChecker
{
    private static final FiniteDuration INTERVAL = Duration.create(10, TimeUnit.SECONDS);
    
    private ActorSystem actorSystem;
    private ExecutionContext executionContext;
    
    @Inject
    MailerClient mailerClient;
    
    @Inject
    Configuration configuration;
    
    @Inject
    HttpExecutionContext httpExecutionContext;
    
    @Inject
    MessagesApi messagesApi;
    
    @Inject
    public UserChecker(ActorSystem actorSystem, ExecutionContext executionContext)
    {
        this.actorSystem = actorSystem;
        this.executionContext = executionContext;
        
        this.initialize();
    }

    private void initialize()
    {
        actorSystem.scheduler().schedule(
            Duration.create(0, TimeUnit.SECONDS),
            INTERVAL,
            () -> check(),
            executionContext
        );
    }
    
    private void check()
    {
        for (User user : User.findOutdatedPrepay())
        {            
            Email email = new Email();
            email.setSubject("Subject");
            email.setFrom(Format.pretty(configuration.getString("coordinates.name"), configuration.getString("play.mailer.user")));
            email.addTo("test@me.com");
            
            CompletableFuture.runAsync(() ->
            {
                try
                {
                    Messages messages = null;
                    
                    if (user.getLanguage() == null)
                    {
                        messages = new Messages(new Lang(new Locale(configuration.getStringList("play.i18n.langs").get(0))), messagesApi);
                    }
                    else
                    {
                        messages = new Messages(new Lang(new Locale(user.getLanguage())), messagesApi);
                    }
                    
                    email.setBodyHtml(prepayNotRefundedWarning.render(user, configuration, messages).body());
                    
                    mailerClient.send(email);
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
            },
            httpExecutionContext.current());
        }
    }
}

And then in the template:

@(user: User, configuration: play.Configuration)(messages: Messages)
@import helpers.Format

@amount = {
    font-size: 30px;
    font-weight: 300;
}

@style = {
    .amount { @amount }
}

@layouts.email(configuration, true, style) {
    <table border="0" cellpadding="5" width="100%">
        <tr>
            <td align="right" valign="top" width="50%"><strong>@messages("form.user") :</strong></td>
            <td align="left" valign="top">@user.getLabel()</td>
        </tr>
        <tr>
            <td align="right" valign="top" width="50%"><strong>@messages("form.user.building") :</strong></td>
            <td align="left" valign="top">@user.getAddress1()<br>@user.getPostalCode() @user.getCity()</td>
        </tr>
        @if(user.getEmail() != null && !user.getEmail().isEmpty()) {
            <tr>
                <td align="right" valign="top" width="50%"><strong>@messages("form.user.email") :</strong></td>
                <td align="left" valign="top">@user.getEmail()</td>
            </tr>
        }
        @if(user.getMobile() != null && !user.getMobile().isEmpty()) {
            <tr>
                <td align="right" valign="top"><strong>@messages("form.user.mobile") :</strong></td>
                <td align="left" valign="top">@user.getMobile()</td>
            </tr>
        }
        <tr>
            <td align="right" valign="top"><strong>@messages("form.user.lastRefill"):</strong></td>
            <td align="left" valign="top">@Format.date(messages("format.dateTime"), user.getLastRefill())</td>
        </tr>
        <tr>
            <td align="right" valign="top"><strong>@messages("form.user.credit") :</strong></td>
            <td align="left" valign="top"><div style="@amount" class="amount">@messages("currency.swissFranc.short") @Format.price(user.getCredit())</div></td>
        </tr>
        <tr>
            <td align="right" valign="top"><strong>@messages("form.user.prepay") :</strong></td>
            <td align="left" valign="top"><div style="@amount" class="amount">@messages("currency.swissFranc.short") @Format.price(user.getPrepayAmount())</div></td>
        </tr>
        <tr>
            <td align="right" valign="top"><strong>@messages("form.user.fees"):</strong></td>
            <td align="left" valign="top"><div style="@amount" class="amount">@messages("currency.swissFranc.short") @Format.price(configuration.getInt("eeproperty.tax.prepayReminder"))</div></td>
        </tr>
        <tr>
            <td align="right" valign="top"><strong>@messages("form.user.total") :</strong></td>
            <td align="left" valign="top"><div style="@amount" class="amount"><u>@messages("currency.swissFranc.short") @Format.price(user.getPrepayAmount() + configuration.getInt("eeproperty.tax.prepayReminder"))</u></div></td>
        </tr>
    </table>
}

But I still have an issue with the lack of HTTP Context because of the reverse routing.

java.lang.RuntimeException: There is no HTTP Context available from here.
	at play.mvc.Http$Context.current(Http.java:62)
	at play.mvc.Http$Context$Implicit.lang(Http.java:359)
	at views.html.layouts.email_Scope0$email.apply(email.template.scala:112)
	at views.html.mail.prepayNotRefundedWarning_Scope0$prepayNotRefundedWarning.apply(prepayNotRefundedWarning.template.scala:58)
	at views.html.mail.prepayNotRefundedWarning_Scope0$prepayNotRefundedWarning.render(prepayNotRefundedWarning.template.scala:108)
	at views.html.mail.prepayNotRefundedWarning.render(prepayNotRefundedWarning.template.scala)
	at actors.UserChecker.lambda$check$1(UserChecker.java:107)
	at java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1626)
	at play.core.j.HttpExecutionContext$$anon$2.run(HttpExecutionContext.scala:56)
	at akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:39)
	at akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinTask.exec(AbstractDispatcher.scala:415)
	at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
	at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
	at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
	at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)

As @marcospereira mentioned above, there is no request available, so I guess you would not have the context.
Can you associate the user selected language with the user object? That way it would be available when you iterate over users in the for loop?

@aditya

user.getLanguage() returns the language code (eg: “en”) from the database as a string.
The language seems to work with that workaround as @messages() gets the right text from the “messages.xx” file where “xx” is the language code.

However, as you pointed out, I don’t have a request object in my actor so I cannot perform usual jobs like reverse routing in my template…
Is there a way to make it work? Maybe creating a fake request object?

I am not sure how creating a fake request would help. Instead how about exposing the email sender utility as an endpoint? Make it extend controller, and move the actor based scheduling logic to the client code which after the periodic frequency makes a call to the endpoint which finds the users to send email to?

That way you will have the request object and it will be much more natural than trying to fake a request?

OK I’ll do that but it sounds pretty dirty…

Hello, I’ve already implemented scheduling sending emails by configuring React frontend code to send requests to Scala Play backend code as a part of newsletter subscription service. You can have a look at it on the Github: