Mail#
jimble sends mail over SMTP (since 2.4.0). No dependency was added: SMTP and MIME are written with the JDK alone.
import io.jimble.util.mail.MailMessage;
import io.jimble.util.mail.Mailer;
Mailer.send(new MailMessage()
.to("hanako@example.co.jp", "Hanako Yamada")
.subject("Your request was approved")
.text("Dear Ms Yamada,\n\nYour expense request (No.123) was approved."));
| Recipients | to / cc / bcc (never written into the headers) / replyTo. Without a From, mail.from from the settings is used |
| Body | text(...), html(...). With both, it is sent as multipart/alternative so the mail client can choose |
| Attachments | attach("invoice.pdf", bytes, "application/pdf"). Non-ASCII file names work as they are |
| Headers | header("List-Unsubscribe", "<mailto:...>"). Headers jimble writes (From, Subject, ...) cannot be set |
| Charset | UTF-8. Non-ASCII subjects and names use RFC 2047; bodies use base64 |
A subject, name or header containing a newline, or an address containing angle brackets or spaces, throws as soon as it is built (to stop header injection). Passing user input straight through cannot add a Bcc.
Trap
HTML-only mail tends to land in spam. Include text(...) when you send HTML.
If user input goes into the HTML, escape it in the template (jte) first.
Send from MQ#
Do not send directly while handling a request. Queue it in MQ and send from execute().
- SMTP is slow (hundreds of milliseconds to seconds) and sometimes fails; the request does not wait
- It is queued in the same transaction. Mail never goes out for a request that was not saved
- If it fails, MQ retries
public MqStatus execute (DB db, Data row) {
Data data = row.getDataOptional("data");
try {
Mailer.send(new MailMessage()
.to(data.getString("to"))
.subject("Your request was approved")
.text(data.getString("body")));
return MqStatus.completed;
} catch (MailException ex) {
if (ex.isTransient()) {
throw ex; // transient (server busy, unreachable) -> retried up to maxRetry()
}
Log.error(ex, "Could not send the mail (retrying will not help)");
return MqStatus.dead; // no such recipient, authentication failed -> do not retry
}
}
MailException.isTransient() is true for SMTP 4xx replies and for connection failures or drops.
It is false for 5xx replies (no such recipient, authentication failed) and for malformed messages.
The same mail can arrive twice. MQ does not promise "exactly once" (MQ). If the process dies after sending but before writing
completed, it sends again. For important notices, check a sent record before sending.
Settings#
mail {
transport = "smtp" # smtp | log | memory
from = "noreply@example.com" # the sender for mail without a From
from_name = "Approval workflow"
smtp {
host = "smtp.example.com"
port = 587 # without it, from security (starttls 587 / tls 465 / none 25)
security = "starttls" # starttls | tls | none
username = "apikey"
password = ""
password = ${?SMTP_PASSWORD}
}
}
Amazon SES, SendGrid, Google Workspace and the like all accept SMTP (STARTTLS on 587 with a user name and password).
| Key | Default | |
|---|---|---|
mail.smtp.connect_timeout |
10s |
how long to wait to connect |
mail.smtp.timeout |
30s |
how long to wait for each read or write |
mail.smtp.helo |
this machine's name | the name given in EHLO |
mail.smtp.envelope_from |
From | where bounces go |
Refused for safety
| STARTTLS chosen but not offered by the server | It does not carry on unencrypted; it throws |
| The TLS certificate's host name does not match | Nothing is sent |
Authenticating over an unencrypted connection (security = "none") |
Refused when the settings are read (the password would travel in clear) |
| Any one recipient refused | Nothing is sent to anyone (a partial delivery means a retry sends again to those who got it) |
Locally and in tests#
transport defaults to smtp. Sending without mail.smtp.host throws (so mail never silently fails to go out).
# conf/application.local.conf
mail.transport = "log" # do not send; log the recipients, subject and body (WARN)
In tests, collect what was sent in memory and check it.
MemoryTransport memory = new MemoryTransport();
Mailer.use(memory);
// ... approve the request ...
assertEquals("Your request was approved", memory.sent().get(0).subject());
Mailer.reset();
Sending through an HTTP API#
To send through a service's HTTP API instead of SMTP, implement MailTransport and swap it in.
Mailer.use(message -> {
byte[] raw = message.toMime(); // the raw mail (an API that takes raw mail can use it as is)
// ... call the API; throw MailException on failure (isTransient = true if worth retrying) ...
});
Not included#
| Receiving (IMAP / POP3) | Sending only |
| ISO-2022-JP | UTF-8 only (every current mail client and phone reads UTF-8) |
| Images embedded as attachments in HTML (cid) | Not yet. Reference images by URL |
| Templates | The body is a string. Pass something built with jte |