Send email from Java with Jakarta Mail, Simple Java Mail, or a notification API. Current Maven coordinates and code that compiles, plus how to choose.
Updated Sep 11, 2026
Last updated: September 2026. Package versions verified against Maven Central on 2026-09-11.
Java has no email support in the JDK itself, so sending mail means adding a library. There are three realistic ways to do it, and they differ mostly in how much SMTP plumbing ends up in your code.
A warning specific to Java before you start: the namespace moved. Jakarta EE 9 renamed the whole platform from javax.* to jakarta.*, so every javax.mail import you find in an older tutorial is now jakarta.mail. If a snippet will not resolve, check that before anything else.
Properties and Session, roughly a third of the code.Jakarta Mail (formerly JavaMail) is the standard Java API for mail. It sends, and it also reads over IMAP and POP3, which is why it is heavier than a send-only library.
Get the dependency from Maven Central. You need two artifacts: the API and an implementation. Do not download loose mail.jar or activation.jar files from third-party jar-hosting sites, which is what a lot of older tutorials suggest; you have no way to verify what is in them, and Maven resolves the pair correctly for you.
What you need first
Add the dependencies
<dependency><groupId>jakarta.mail</groupId><artifactId>jakarta.mail-api</artifactId><version>2.1.5</version></dependency><dependency><groupId>org.eclipse.angus</groupId><artifactId>angus-mail</artifactId><version>2.0.5</version><scope>runtime</scope></dependency>
Verified against jakarta.mail:jakarta.mail-api 2.1.5 and org.eclipse.angus:angus-mail 2.0.5. Angus Mail is the maintained successor to the old com.sun.mail implementation.
Send the message
import jakarta.mail.Authenticator;import jakarta.mail.Message;import jakarta.mail.MessagingException;import jakarta.mail.PasswordAuthentication;import jakarta.mail.Session;import jakarta.mail.Transport;import jakarta.mail.internet.InternetAddress;import jakarta.mail.internet.MimeMessage;import java.util.Properties;public class SendMail {public static void main(String[] args) throws MessagingException {String from = "orders@example.com";String to = "erika@example.com";Properties properties = new Properties();properties.put("mail.smtp.host", "smtp.example.com");properties.put("mail.smtp.port", "587");properties.put("mail.smtp.auth", "true");properties.put("mail.smtp.starttls.enable", "true");Session session = Session.getInstance(properties, new Authenticator() {@Overrideprotected PasswordAuthentication getPasswordAuthentication() {// The SMTP username is the sending account, not the recipient.return new PasswordAuthentication(System.getenv("SMTP_USERNAME"),System.getenv("SMTP_PASSWORD"));}});MimeMessage message = new MimeMessage(session);message.setFrom(new InternetAddress(from));message.addRecipient(Message.RecipientType.TO, new InternetAddress(to));message.setSubject("Your order shipped");message.setContent("<h1>Your order shipped</h1><p>Track it in the app.</p>","text/html; charset=utf-8");Transport.send(message);}}
Three details that matter more than they look:
setContent with a MIME type, not setText. setText sends plain text, so HTML passed to it arrives as visible tags.mail.smtp.port to 465 while enabling starttls is a common misconfiguration that hangs.System.getenv. The old version of this page hard-coded a Gmail address and "*******" as the password, and used the recipient's address as the SMTP username.Where it gets thin: roughly twenty lines of configuration before you compose anything, an SMTP-level accept or reject as your only feedback, and no delivery status, bounce handling, or templates.
Simple Java Mail wraps Jakarta Mail in a builder API. It is the same transport underneath, with the Properties and Session ceremony removed. It is send-only, so if you need to read mail you are back to Jakarta Mail.
What you need first
Add the dependency
<dependency><groupId>org.simplejavamail</groupId><artifactId>simple-java-mail</artifactId><version>9.3.4</version></dependency>
Verified against org.simplejavamail:simple-java-mail 9.3.4. It brings its own Jakarta Mail implementation, so you do not add Angus Mail alongside it.
Send the message
import org.simplejavamail.api.email.Email;import org.simplejavamail.api.mailer.Mailer;import org.simplejavamail.api.mailer.config.TransportStrategy;import org.simplejavamail.email.EmailBuilder;import org.simplejavamail.mailer.MailerBuilder;public class SendMail {public static void main(String[] args) {Email email = EmailBuilder.startingBlank().from("Acme", "orders@example.com").to("Erika", "erika@example.com").to("Ops", "ops@example.com").withSubject("Your order shipped").withHTMLText("<h1>Your order shipped</h1><p>Track it in the app.</p>").buildEmail();Mailer mailer = MailerBuilder.withSMTPServer("smtp.example.com",587,System.getenv("SMTP_USERNAME"),System.getenv("SMTP_PASSWORD")).withTransportStrategy(TransportStrategy.SMTP_TLS).buildMailer();mailer.sendMail(email);}}
Note the chain runs unbroken from MailerBuilder to .buildMailer(). The old version of this page put a semicolon after .withTransportStrategy(...) and then continued with .buildMailer() on the next line, which does not compile.
withHTMLText for HTML, withPlainText for text, or call both to send a multipart message with a plain-text fallback. Calling .to(...) more than once adds recipients rather than replacing them.
On transport strategy: SMTP_TLS is STARTTLS on 587, and SMTPS is implicit TLS on 465. Match the strategy to the port, or the connection will hang.
Where it gets thin: send-only, and everything above delivery is still yours. It is a nicer way to write the same SMTP call, not a different capability.
Both options above solve delivery. Here is what tends to arrive in the fortnight afterward:
A notification API such as Courier sits above your email provider and handles those. You keep your SMTP relay or provider delivering, and your Java stops knowing which one it is.
What the layer gives you:
Set it up
Create a free account: the Developer plan includes 10,000 messages a month, and past that it's $0.005 per message, the same rate on every channel. Then connect an email provider under Channels using the credentials it already gave you. Same relay or provider as the sections above; you are not changing who delivers.
Add the dependency
<dependency><groupId>com.courier</groupId><artifactId>courier-java</artifactId><version>6.5.0</version></dependency>
Or with Gradle:
implementation("com.courier:courier-java:6.5.0")
Verified against com.courier:courier-java 6.5.0. Do not copy a 1.x snippet; the API is entirely different and will not compile.
Send the message
import com.courier.client.CourierClient;import com.courier.client.okhttp.CourierOkHttpClient;import com.courier.models.ElementalContentSugar;import com.courier.models.UserRecipient;import com.courier.models.send.SendMessageParams;public class SendMail {public static void main(String[] args) {// Reads COURIER_API_KEY from the environment.CourierClient client = CourierOkHttpClient.fromEnv();SendMessageParams params = SendMessageParams.builder().message(SendMessageParams.Message.builder().to(UserRecipient.builder().email("erika@example.com").build()).content(ElementalContentSugar.builder().title("Your order shipped").body("Track it anytime in the app.").build()).build()).build();var response = client.send().message(params);System.out.println(response.requestId());}}
The version you will run in production references a template you published in Courier and addresses the user rather than a raw address, so their channel preferences apply:
SendMessageParams params = SendMessageParams.builder().message(SendMessageParams.Message.builder().to(UserRecipient.builder().userId("user-123").build()).template("ORDER_SHIPPED").build()).build();
Nothing in that call names email. Routing and the user's stored preferences decide the channel, so moving a notification to push, or sending both, is configuration rather than a recompile.
The SDK throws typed unchecked exceptions, so you catch by status:
try {var response = client.send().message(params);System.out.println(response.requestId());} catch (com.courier.errors.RateLimitException e) {// back off and retry} catch (com.courier.errors.BadRequestException e) {// your payload is wrong; retrying will not helpSystem.err.println(e.getMessage());}
CourierServiceException is the base for HTTP errors, with a subclass per status code. Retry RateLimitException and InternalServerException; do not retry BadRequestException, which will fail identically every time.
Where it gets thin: one more service in the path, and a concept to learn before your first send. If all you need is one SMTP send from a batch job, Simple Java Mail is less machinery.
If you are writing this in Cursor, Claude Code, or Codex, connect the agent to Courier directly instead of pasting snippets. It then works from the current SDK shapes rather than the 2022-era API that most training data still contains, which is the difference between Java that compiles and Java that does not.
The MCP server gives the agent typed tool calls. In Claude Code:
claude mcp add --transport http courier https://mcp.courier.com --header api_key:YOUR_API_KEY
Courier Skills adds the practices that are easy to get wrong, such as never batching a one-time passcode:
npx skills add trycourier/courier-skills
Both sit alongside the Courier CLI for scripting and CI. courier.com/agents collects the setup for Cursor, Claude Code, Codex, and VS Code in one place, and Build with AI has the machine-readable docs index an agent can fetch on its own.
Then ask in plain language: "email a receipt when a payment succeeds, and respect the user's preferences." The agent picks the right primitive, and knows which channels need a recorded opt-in.
This matters for the same reason this guide version-stamps every snippet. An agent working from stale training data will confidently write a send call that no longer exists.
| Jakarta Mail | Simple Java Mail | Notification API | |
|---|---|---|---|
| Maven artifact | jakarta.mail:jakarta.mail-api 2.1.5 + angus-mail 2.0.5 | org.simplejavamail:simple-java-mail 9.3.4 | com.courier:courier-java 6.5.0 |
| Lines to a send | ~25 | ~8 | ~12 |
| Can read mail | yes, IMAP and POP3 | no | no |
| Transport | SMTP | SMTP | HTTPS |
| Blocked-port risk | real, 25 and 587 | real, 25 and 587 | no |
| Delivery status | accept or reject only | accept or reject only | one log, per-message status |
| Typed exceptions per status | no | no | yes |
| Templates outside your code | no | no | yes |
| Preferences and unsubscribes | you build it | you build it | built in |
| Other channels | none | none | SMS, push, in-app, chat |
| Best for | reading mail, or standards compliance | straightforward SMTP sending | email plus other channels |
If you have SMTP credentials and one email to send, use Simple Java Mail and stop reading. If email is becoming a feature of a JVM application, the argument for the notification layer is stronger than in most languages: getting copy out of compiled code and into templates means wording changes stop requiring a release.
FAQ
Session from SMTP properties and an Authenticator, compose a MimeMessage with sender, recipient, subject, and body, then call Transport.send(message). That is the low-level path. Simple Java Mail wraps it in a builder API, and a notification API removes SMTP from your code entirely.jakarta.mail. The javax.mail namespace was renamed when Jakarta EE 9 moved the whole platform from javax.* to jakarta.*, and com.sun.mail:javax.mail is frozen at 1.6.2. Current code uses jakarta.mail:jakarta.mail-api for the API plus org.eclipse.angus:angus-mail for the implementation. Every import changes from javax.mail.* to jakarta.mail.*.jakarta.mail:jakarta.mail-api at 2.1.5 with org.eclipse.angus:angus-mail at 2.0.5 as the runtime implementation. For Simple Java Mail, org.simplejavamail:simple-java-mail at 9.3.4, which brings its own implementation. For Courier as a notification layer, com.courier:courier-java at 6.5.0. Do not download jars from third-party sites.Courier.init(), SendEnhancedRequestBody, and new SendService().sendEnhancedMessage() belonged to courier-java 1.x. The current SDK is 4.x: build a client with CourierOkHttpClient.fromEnv(), construct SendMessageParams through nested builders, and call client.send().message(params). Bump the dependency to 6.5.0 and rewrite the call.Properties object, a Session, and an Authenticator before composing anything, and it can read mail as well as send it. Simple Java Mail is a wrapper around it with a builder API, so a send is about six readable lines instead of twenty. It is send-only. If you are just sending mail, the wrapper is less code to get wrong.Keep exploring
One API, every channel
Courier gives you one API for email, SMS, push, and chat, with templates, routing, retries, and delivery logs built in.
Last updated Sep 11, 2026. Code samples are illustrative; provider APIs and pricing change over time, so check each provider’s docs before relying on them.