How to send email using Jakarta?
TL;DR
Transitioning from JavaMail to Jakarta Mail
Ever felt like you finally mastered a library only for the developers to go and change the name on you? That’s exactly what happened when JavaMail turned into Jakarta Mail, and honestly, it’s been a bit of a headache for those of us maintaining legacy code.
The big shift happened because Oracle handed things over to the Eclipse Foundation. (Why did Oracle give rights for Java EE to Eclipse foundation? - Reddit) Since they couldn't keep using the "Java" trademark for the packages, everything moved.
- The Namespace Swap: You gotta change
javax.mailtojakarta.mail. It sounds small, but it breaks every import in your project. - Eclipse Foundation Takeover: This move was all about making the project more community-driven. According to The Eclipse Foundation, this transition was part of the larger move to Jakarta EE to keep the ecosystem open.
- SMTP Compatibility: Most old servers don't care about your package names, but your build tools sure do. If you mix
javaxandjakartadependencies, your classpath will basically explode.
I've seen developers in retail and finance spend days hunting down hidden javax references in giant pom.xml files. It's tedious, but necessary if you want to use the latest features.
What actually changed in the api?
If you're worried about learning a whole new system, don't be. The api is basically identical to the old one. The classes you know—like MimeMessage, Session, and Transport—still works exactly the same way. The only real "change" is the import statement at the top of your file.
| Old JavaMail (javax) | New Jakarta Mail (jakarta) |
|---|---|
javax.mail.Session |
jakarta.mail.Session |
javax.mail.internet.MimeMessage |
jakarta.mail.internet.MimeMessage |
javax.mail.Transport |
jakarta.mail.Transport |
If you use an IDE like IntelliJ or Eclipse, a simple "Find and Replace" across your whole project usually fixes 99% of the issues.
Setting up your environment for Jakarta
Getting your environment ready for Jakarta Mail is mostly about not letting your build file turn into a crime scene. If you're moving from older Java versions, the biggest hurdle is making sure you don't accidentally pull in two versions of the same library—which happens more than I'd like to admit.
First thing, you need the jakarta.mail-api. But here is the kicker: the api alone doesn't actually send anything. You need an implementation. Eclipse Angus is the one you want. It’s important to know that Angus is just the new name for the original Sun/Oracle implementation. If you used to use com.sun.mail, Angus is the direct successor, so it’s the most stable "engine" under the hood.
- Add the API: This defines the classes you'll use in your code.
- Include Angus: Without this provider, you'll get a "No provider for jakarta.mail.util.StreamProvider" error that'll ruin your afternoon.
- Watch for javax: If you're working on a legacy healthcare or retail app, check your dependency tree. One stray
javax.mailjar from an old library and your app will crash on startup.
<dependency>
<groupId>jakarta.mail</groupId>
<artifactId>jakarta.mail-api</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.eclipse.angus</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.2</version>
<scope>runtime</scope>
</dependency>
Once the jars are in place, you gotta tell Jakarta how to talk to the server. Most modern setups (like for fintech or secure portals) use Port 587 with TLS. Don't use 465 unless you're dealing with really old systems.
Always set mail.smtp.timeout and mail.smtp.connectiontimeout. I once saw a production server hang for hours because it was waiting for a response from a dead smtp relay that never came. Setting a 5-second timeout saves lives.
Writing the code to send your first mail
Now that the environment is ready, we need to pass those configuration properties into a Session object, which acts as the main entry point for the api.
Everything in jakarta mail revolves around the Session. Think of it as the environment where your mail lives before it's sent. You usually want to use an Authenticator so you don't have to hardcode passwords in weird places.
I've seen plenty of junior devs forget to handle MessagingException. If your smtp server is down or the credentials are wrong, your app will just hang or crash if you don't wrap this in a proper try-catch block. In a fintech app, for example, a failed notification could mean a missed fraud alert, so logging those errors is huge.
Properties props = new Properties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.starttls.required", "true"); // make sure tls is actually used
props.put("mail.smtp.port", "587"); // standard port for modern smtp
Session session = Session.getInstance(props, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("[email protected]", "password123");
}
});
try {
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress("[email protected]"));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse("[email protected]"));
message.setSubject("Your Order Update");
message.setText("Hey, your package is on the way!");
Transport.send(message);
} catch (MessagingException e) {
e.printStackTrace(); // do better than this in production pls
}
Plain text is fine for a quick alert, but if you're building a retail newsletter or a healthcare portal sending lab results, you'll need MimeMessage. You have to set the content-type to text/html or your users will just see raw tags.
For attachments, you use MimeBodyPart. You basically build the email like a lego set—one part for the text, one for the image, and then shove them into a Multipart container.
try {
MimeMessage message = new MimeMessage(session);
message.setSubject("Results are in");
<span class="hljs-type">MimeBodyPart</span> <span class="hljs-variable">textPart</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">MimeBodyPart</span>();
textPart.setContent(<span class="hljs-string">"<h1>Results are in</h1><p>Check the pdf.</p>"</span>, <span class="hljs-string">"text/html"</span>);
<span class="hljs-type">MimeBodyPart</span> <span class="hljs-variable">attachmentPart</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">MimeBodyPart</span>();
attachmentPart.attachFile(<span class="hljs-keyword">new</span> <span class="hljs-title class_">File</span>(<span class="hljs-string">"report.pdf"</span>));
<span class="hljs-type">Multipart</span> <span class="hljs-variable">multipart</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">MimeMultipart</span>();
multipart.addBodyPart(textPart);
multipart.addBodyPart(attachmentPart);
message.setContent(multipart);
Transport.send(message);
} catch (Exception e) {
System.out.println("Failed to send attachment: " + e.getMessage());
}
Honestly, keep it simple. The more complex the html, the more likely a random api or spam filter marks it as junk.
Testing your Jakarta email workflow
Testing your code is the most stressful part of the whole journey, honestly. You think you've nailed the jakarta mail logic, but then you hit "send" and... nothing. No email, no error, just a void.
I've seen too many devs use their personal gmail for testing. Bad idea. If you fire off 500 test emails while debugging a loop, google is gonna flag your domain faster than you can say "spam."
- Avoid the spam trap: Real providers have strict rate limits. If you're testing a retail blast or a healthcare notification system, use disposable addresses.
- Disposable mail: Tools like Mail7 are great because they give you an api to automate the whole thing.
- Automation: Instead of manual testing, use a library like GreenMail. It starts a fake smtp server in your unit tests so you can verify the email was "sent" without actually hitting the internet.
Here is a quick example of how you'd test this with GreenMail:
@Test
void testEmailSending() {
GreenMail greenMail = new GreenMail(ServerSetupTest.SMTP);
greenMail.start();
<span class="hljs-comment">// ... run your sending code here ...</span>
MimeMessage[] receivedMessages = greenMail.getReceivedMessages();
assertEquals(<span class="hljs-number">1</span>, receivedMessages.length);
assertEquals(<span class="hljs-string">"Your Order Update"</span>, receivedMessages[<span class="hljs-number">0</span>].getSubject());
greenMail.stop();
}
If your mail isn't moving, you need to see what the server is thinking. Most people don't realize that jakarta mail has a built-in "spy" mode. Just add session.setDebug(true); to your code.
// This is a lifesaver for seeing the raw conversation
session.setDebug(true);
try {
Transport.send(message);
} catch (MessagingException e) {
// check the console logs for "EHLO" or "STARTTLS" errors
System.out.println("Handshake failed, probably a certificate issue.");
}
Reading those logs is how you catch ssl handshake failures. Sometimes the server wants a specific version of tls that your local java doesn't support yet.
Testing isn't just about success; it's about failing gracefully. If your fintech app can't send a password reset, you need to know why immediately. Stick to these tools, and you'll sleep way better.