3 ways to send email with Java

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.

Which option should you pick?

  • Jakarta Mail if you need the standard, or you also need to read mail rather than only send it. It is verbose, and it is the thing the other options are built on.
  • Simple Java Mail if you only need to send. Same underlying library, a builder API instead of Properties and Session, roughly a third of the code.
  • A notification API once email is one channel among several, or when templates, per-user preferences, and a delivery log start mattering more than the send call.

1. Send with Jakarta Mail

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

  • Java 11 or newer.
  • SMTP credentials: host, port, username, and password.
  • For Gmail, an app password rather than your account password, which requires two-factor authentication on the account. Google removed "less secure app access" in 2022.

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() {
@Override
protected 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.
  • STARTTLS on 587, or SSL on 465. Pick one pair. Setting mail.smtp.port to 465 while enabling starttls is a common misconfiguration that hangs.
  • Credentials from 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.

2. Send with Simple Java Mail

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

  • Java 8 or newer.
  • The same SMTP credentials as above. This library changes the code you write, not what your provider requires.

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.

3. Send with a notification API

Both options above solve delivery. Here is what tends to arrive in the fortnight afterward:

  • Copy is a string literal in compiled code, so a wording change is a full deploy.
  • Someone unsubscribes and you now own a suppression list.
  • The same event should reach one user by email and another by push.
  • Your SMTP relay has a bad hour and there is no second path.
  • Twenty events fire in a minute and one user gets twenty emails.
  • Someone asks how many notifications failed last week and you cannot answer.

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:

  • Automatic failover between providers when one starts erroring.
  • Journeys for multi-step sequences, with digest and throttling nodes.
  • One send endpoint for email, SMS, push, in-app, Slack, and Microsoft Teams.
  • Per-user, per-channel preferences enforced before a send.
  • Templates edited outside your codebase, with localization per user. For a JVM application this is the big one, because it takes copy changes off your release cycle.
  • One log covering every channel.

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 help
System.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.

Building this with an AI agent

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.

Comparing the three options

Jakarta MailSimple Java MailNotification API
Maven artifactjakarta.mail:jakarta.mail-api 2.1.5 + angus-mail 2.0.5org.simplejavamail:simple-java-mail 9.3.4com.courier:courier-java 6.5.0
Lines to a send~25~8~12
Can read mailyes, IMAP and POP3nono
TransportSMTPSMTPHTTPS
Blocked-port riskreal, 25 and 587real, 25 and 587no
Delivery statusaccept or reject onlyaccept or reject onlyone log, per-message status
Typed exceptions per statusnonoyes
Templates outside your codenonoyes
Preferences and unsubscribesyou build ityou build itbuilt in
Other channelsnonenoneSMS, push, in-app, chat
Best forreading mail, or standards compliancestraightforward SMTP sendingemail 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.

Next steps


FAQ

Frequently asked questions

Add Jakarta Mail as a Maven dependency, build a 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.

One API, every channel

Ship notifications without the boilerplate

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.