I once spent a long night hunting a phantom bug. The app said the email was sent. The logs agreed. The mailbox stayed empty. Turned out the SMTP server wanted
TLS and I was talking plain old port 25 like it was still dial up time. Switched to
STARTTLS on 587 and the floodgates opened. Since then I treat email as a feature that deserves a checklist, not an afterthought.
Send email with JavaMail without drama
Email from Java is simple on the surface. Set some properties, authenticate, build a
MimeMessage, press send. The part that bites is
TLS vs
SSL and which port the server wants. Many providers expect
STARTTLS on 587. Some still listen on
465 with SSL from the first byte. If you use Gmail or a corporate relay that enforces encryption, set the right flags or your code will look fine and do nothing. Here is the bare minimum that works today on common SMTP servers.
Properties props = new Properties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true"); // STARTTLS on 587
props.put("mail.smtp.host", "smtp.example.com");
props.put("mail.smtp.port", "587");
props.put("mail.smtp.connectiontimeout", "5000");
props.put("mail.smtp.timeout", "8000");
Session session = Session.getInstance(props, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("user@example.com", "secret");
}
});
// session.setDebug(true);
Message msg = new MimeMessage(session);
msg.setFrom(new InternetAddress("no-reply@example.com", "Example App"));
msg.setRecipients(Message.RecipientType.TO,
InternetAddress.parse("friend@domain.com", false));
msg.setSubject("Your receipt");
msg.setSentDate(new java.util.Date());
msg.setText("Thanks for your order. We will let you know when it ships.");
Transport.send(msg);
Once you can send one email, you will want
templates. Text copy changes. Headers change. Buyers want HTML with a plain text fallback. You can keep it simple with
String.format for early drafts, then graduate to a template engine.
FreeMarker and
Velocity are both solid. FreeMarker tends to be strict, which is good for catching missing variables. It also plays well with UTF 8, so your subject lines can carry accents without weird symbols.
// FreeMarker setup
Configuration cfg = new Configuration();
cfg.setClassForTemplateLoading(MyMailer.class, "/mail/");
cfg.setDefaultEncoding("UTF-8");
// data model
Map<String, Object> model = new HashMap<>();
model.put("name", "Guillermo");
model.put("orderId", 12345);
model.put("total", "49.90");
// render
Template tpl = cfg.getTemplate("receipt.ftl"); // stored in resources /mail/
StringWriter out = new StringWriter();
tpl.process(model, out);
String html = out.toString();
// build message with text and HTML
MimeMultipart alt = new MimeMultipart("alternative");
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText("Hi " + model.get("name") + ", your order " + model.get("orderId") + " is paid.", "UTF-8");
alt.addBodyPart(textPart);
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(html, "text/html; charset=UTF-8");
alt.addBodyPart(htmlPart);
Message msg = new MimeMessage(session);
msg.setFrom(new InternetAddress("no-reply@example.com"));
msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse("buyer@domain.com", false));
msg.setSubject("Receipt " + model.get("orderId"), "UTF-8");
msg.setContent(alt);
Transport.send(msg);
TLS trips teams in two ways. First, folks confuse
SSL on 465 with
STARTTLS on 587. Those are not the same handshake. Pick one and set properties to match. Second, some relays use a cert that your JVM does not trust. You will see a handshake alert in debug and nothing goes out. When you control the relay, import the cert into the JVM truststore. When you cannot, JavaMail has a property to trust a host. Use it only when you fully trust the network path.
// If the server presents a cert your JVM does not know
// Best: import the cert into cacerts with keytool
// Quick fix when you trust the host:
props.put("mail.smtp.ssl.trust", "smtp.example.com");
// For SSL on 465 instead of STARTTLS
props.put("mail.smtp.ssl.enable", "true");
props.put("mail.smtp.port", "465");
props.remove("mail.smtp.starttls.enable");
// Turn on debug to see the handshake and server replies
session.setDebug(true);
Production email is about details. Set
reply to and
return path so bounces do not vanish. Use
Message ID and your own
List Unsubscribe header if you send newsletters. Keep timeouts short and add retries with a small backoff. Encode everything as
UTF 8 and send both
text and
HTML parts for deliverability. Attachments go in a mixed multipart that wraps the alternative part. And watch your content. Words that look spammy will land you in the junk folder no matter how pretty the template is.
// Headers that help in the real world
msg.setHeader("X-Application", "ExampleApp");
msg.setHeader("List-Unsubscribe", "<mailto:unsubscribe@example.com>");
// Return-Path needs SMTP envelope sender, not only header
// With JavaMail, set it on Transport:
// Transport.send(msg, msg.getAllRecipients()); // then set property
props.put("mail.smtp.from", "bounces@example.com");
A few quick checks before you ship. Use a test account at Gmail and at Hotmail to spot formatting quirks. Send yourself a message with long accents to confirm
charset is correct. If your host blocks port 25, try 587 with STARTTLS through your provider. Keep
Session debug on in staging and capture logs so support can answer why a message did not arrive. And stash your SMTP password outside the build file, in an env var or a small properties file that is not in version control.Strong recipe. Use JavaMail with the right TLS setting, pick a template engine you can live with, send both text and HTML, and keep an eye on headers and timeouts. The code above covers the common paths for Gmail, hosted relays, and many corporate servers. Do that and your app will send mail that lands where it should, looks good, and is easy to tweak when marketing asks for a new subject line at 5 pm on a Friday.