← All posts

Send an Email in Java: SMTP & Modern APIs

Send an email in java - Learn how to send an email in Java with Jakarta Mail (SMTP) and modern APIs like Robotomail. Discover solutions for traditional and AI a

Send an Email in Java: SMTP & Modern APIs

Yes, you can send an email in Java. That part is not hard anymore.

What most tutorials still miss is that sending is only the easy half. If you are building an AI agent, a support bot, or an autonomous workflow, email is not a fire-and-forget notification channel. It is a conversation surface. Your Java code has to send, receive, correlate replies, preserve thread context, and act without a human opening an inbox.

That changes the engineering choice. A lot.

Why Most Java Email Tutorials Are Outdated for AI Agents

Most Java email guides still teach the world as if your app only needs to send password resets, alerts, or one-way receipts. That advice is fine for old-school backend systems. It breaks down for agents.

A robot holds an email, looking confused next to a Java coffee cup on a scroll.

The common recipe is familiar. Add Jakarta Mail. Configure SMTP. Authenticate. Build a MimeMessage. Call Transport.send(). You get outbound mail, and the tutorial ends there.

For agent workflows, that is a dead end.

The problem shifted from sending to conversing

A modern agent does not just notify someone. It sends a message, waits for a reply, parses intent from the reply, matches that reply to prior context, and continues the thread. That means your system has to care about mailbox lifecycle, inbound delivery, reply parsing, and thread continuity from day one.

That gap is visible in the ecosystem. A MailerSend survey-style overview of Java email content notes that most tutorials stay focused on outbound SMTP basics and ignore inbound handling. It also cites over 15,000 unresolved or poorly answered Stack Overflow questions tagged around Java email inbound workflows, with 78% lacking complete solutions for threading replies without human intervention (MailerSend on sending email in Java).

Why this matters in agent stacks

If you are using LangChain, CrewAI, or AutoGen, you do not want an agent that can only shout into the void. You want one that can:

  • Open a thread: send the initial message with stable identifiers
  • Interpret a reply: extract sender, body, and useful headers
  • Maintain context: map the response back to the right task or memory
  • Act safely: verify that inbound events are authentic before execution

For AI agents, outbound SMTP is table stakes. The primary challenge begins when the first reply lands.

Traditional Java email tutorials still optimize for “how do I send one message?” The harder question is “how do I run a two-way email loop in production without building a fragile mess?” That is the question worth solving.

Sending Email the Classic Way with Jakarta Mail

If your goal is strictly outbound mail, Jakarta Mail still works. It is the classic Java approach, and it remains the baseline pattern most developers encounter first.

An office worker holding a letter with icons for Maven and Gradle tools representing email configuration.

The core flow is straightforward:

  1. Add the dependency.
  2. Configure SMTP session properties.
  3. Authenticate.
  4. Create a MimeMessage.
  5. Send it.

That is the official shape of the workflow described in Mailtrap’s Java email guide, which also calls out two common production failures: TLS misconfiguration can cause a 35% failure rate on port 587 without starttls.enable=true, and missing DKIM/SPF can cause 20-40% inbox placement loss (Mailtrap Java send email guide).

Add the dependency

Use the Jakarta Mail API dependency in Maven:

<dependency>
  <groupId>jakarta.mail</groupId>
  <artifactId>jakarta.mail-api</artifactId>
  <version>2.1.3</version>
</dependency>

If you are using a framework that already wraps mail transport, check your dependency tree first. Java mail libraries have a habit of bringing in overlapping artifacts.

Configure the SMTP session

This is the part every tutorial shows, and for good reason. If the properties are wrong, nothing else matters.

import jakarta.mail.*;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;

import java.util.Properties;

public class SmtpMailer {
    public static void main(String[] args) throws Exception {
        String username = System.getenv("MAIL_USERNAME");
        String password = System.getenv("MAIL_PASSWORD");
        String to = "[email protected]";

        Properties props = new Properties();
        props.put("mail.smtp.host", "smtp.gmail.com");
        props.put("mail.smtp.port", "587");
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.starttls.enable", "true");

        Session session = Session.getInstance(props, new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(username, password);
            }
        });

        MimeMessage message = new MimeMessage(session);
        message.setFrom(new InternetAddress(username));
        message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
        message.setSubject("Java SMTP test");
        message.setText("Hello from Jakarta Mail.");

        Transport.send(message);
    }
}

That snippet is enough to send a plain-text email.

Build richer messages

Real email rarely stays plain text for long. You usually need HTML, attachments, or both.

import jakarta.mail.*;
import jakarta.mail.internet.*;

import java.util.Properties;

public class HtmlMailer {
    public static void main(String[] args) throws Exception {
        String username = System.getenv("MAIL_USERNAME");
        String password = System.getenv("MAIL_PASSWORD");

        Properties props = new Properties();
        props.put("mail.smtp.host", "smtp.gmail.com");
        props.put("mail.smtp.port", "587");
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.starttls.enable", "true");

        Session session = Session.getInstance(props, new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(username, password);
            }
        });

        MimeMessage message = new MimeMessage(session);
        message.setFrom(new InternetAddress(username));
        message.setRecipients(
            Message.RecipientType.TO,
            InternetAddress.parse("[email protected],[email protected]")
        );
        message.setSubject("HTML email from Java");

        MimeBodyPart htmlPart = new MimeBodyPart();
        htmlPart.setContent("<html><body><h1>Hello</h1><p>This is HTML.</p></body></html>", "text/html");

        Multipart multipart = new MimeMultipart();
        multipart.addBodyPart(htmlPart);

        message.setContent(multipart);

        Transport.send(message);
    }
}

Notice the use of InternetAddress.parse(...) for comma-separated recipients. That is one of the small but useful parts of the API.

A quick walkthrough helps if you want to see the library in action:

What works and what does not

Jakarta Mail is fine when you control the environment and the use case is narrow.

Use case Fit for Jakarta Mail
Internal alerts Good
Transactional notifications Acceptable
Rich HTML emails Good, with more boilerplate
Attachments Supported, but verbose
Autonomous reply loops Poor fit

The rough edges show up fast:

  • Authentication friction: Gmail-style SMTP setups often require app passwords and extra account security steps.
  • TLS mistakes: Port 587 without STARTTLS is a common foot-gun.
  • Deliverability burden: With SMTP, you own more of the sending hygiene.
  • Error handling: MessagingException tends to hide a lot of failure modes behind one family of exceptions.

If you use Jakarta Mail in production, fail loudly on auth and TLS issues. Silent retries on bad configuration waste time.

A pragmatic take

Jakarta Mail is the manual transmission version of email in Java. You get control, but you also inherit setup overhead and operational friction. For a simple app that only needs outbound mail, that trade-off can be fine.

For agents, it becomes technical debt quickly.

The Modern API-First Approach for Agent Workflows

SMTP was designed for email delivery, not for clean application ergonomics. Java developers have adapted to that for years, but agent workflows expose the mismatch fast.

The older model asks your code to think in terms of sessions, SMTP properties, provider quirks, and mailbox credentials. The newer model asks for an authenticated HTTP request with structured payloads and predictable responses. For software, that is the better abstraction.

Infographic

The historical reason is simple. The JavaMail API was standardized in 1998, became foundational for Java email, and still reflects an older integration model. It also carries friction that autonomous systems feel immediately, including manual SMTP setup, OAuth-related flows, and app-password style account workarounds tied to newer provider security policies (DigitalOcean JavaMail example).

What changes with an API

An API-first mail provider shifts the problem from protocol wiring to application logic.

Instead of this:

  • configure SMTP host and port
  • negotiate TLS correctly
  • manage credentials for mailbox access
  • translate failures out of transport exceptions

You do this:

  • issue an HTTP request
  • send JSON
  • receive a structured response
  • handle errors as normal application errors

That difference matters more in Java than people admit. Java codebases get noisy fast when infrastructure concerns bleed into business logic.

Side-by-side trade-offs

Concern SMTP with Jakarta Mail API-first email
Setup shape Session properties and auth API key and endpoint
Payload format MIME assembly JSON request body
Error handling Mail exceptions HTTP status plus response body
Best for Legacy systems, narrow outbound use Programmatic workflows and agents
Threading support Manual Often easier to model upstream

The important point is not that SMTP is obsolete. It is that SMTP is low-level for the problem most agent developers have.

A Java shape that fits agents better

The implementation pattern is usually cleaner because your application can use the same client stack it already uses for every other service. For example, using Java’s built-in HTTP client:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ApiMailer {
    public static void main(String[] args) throws Exception {
        String apiKey = System.getenv("MAIL_API_KEY");

        String json = """
        {
          "from": "[email protected]",
          "to": ["[email protected]"],
          "subject": "Status update",
          "text": "Your request has been processed."
        }
        """;

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.example-mail-provider.test/messages"))
            .header("Authorization", "Bearer " + apiKey)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(json))
            .build();

        HttpClient client = HttpClient.newHttpClient();
        HttpResponse<String> response =
            client.send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.statusCode());
        System.out.println(response.body());
    }
}

That example is intentionally generic, because the architectural point matters more than any one vendor SDK. The mail operation becomes another service call.

For a deeper argument on why email should look like an API for software, this piece on an API for email gets at the core design difference.

What works better in practice

For agent systems, API-first mail has a few practical advantages:

  • Structured responses: easier for orchestration code to reason about
  • Cleaner auth model: your app manages service credentials, not mailbox login rituals
  • Lower incidental complexity: less protocol-specific code in your Java service
  • Better composability: easier to plug into queues, workers, and tool-calling agents

What does not work as well is pretending outbound send is the whole problem. If your architecture ends at “message accepted,” you still have not solved email for agents.

Receiving Replies and Maintaining Conversations

This is the part most Java email articles skip.

Sending a message is easy. Handling the reply is where autonomous systems either become useful or collapse into a pile of polling jobs, half-parsed MIME payloads, and broken thread matching.

A friendly robot looks at a computer screen displaying various email reply options with a question mark bubble.

Why IMAP polling is usually the wrong default

A lot of teams start by saying, “We already know Java. We can just poll a mailbox.” Technically, yes.

Operationally, it is usually the wrong shape for agents.

Polling introduces lag, state management, duplicate processing concerns, and more credential handling. You also end up doing a lot of manual work to turn raw inbound mail into application events.

Webhook delivery is usually the cleaner pattern. Your application receives an HTTP request when a message arrives, verifies the signature, parses the payload, and pushes the event into your workflow engine.

A simple webhook endpoint in Spring Boot

Here is the shape of an inbound endpoint:

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/webhooks/email")
public class EmailWebhookController {

    @PostMapping
    public ResponseEntity<String> receiveEmail(
            @RequestHeader(value = "X-Signature", required = false) String signature,
            @RequestBody String payload) {

        if (!isValidSignature(signature, payload)) {
            return ResponseEntity.status(401).body("invalid signature");
        }

        System.out.println("Inbound payload: " + payload);

        return ResponseEntity.ok("accepted");
    }

    private boolean isValidSignature(String signature, String payload) {
        // Replace with your HMAC verification logic.
        return signature != null && !signature.isBlank();
    }
}

The key point is not the framework. Use Spring Boot, Javalin, Micronaut, or plain servlets if you want. The key point is the event flow.

Never let an agent act on inbound email before verifying the webhook signature. Email content is untrusted input.

If you want a concrete reference for the send-receive model, this guide on receive and reply shows the kind of workflow agent systems need.

What to extract from inbound mail

Do not stop at sender and body. For conversation continuity, the headers matter.

At minimum, parse and persist:

  • From: who replied
  • Subject: useful, but not enough for threading
  • Body text and HTML: for the agent’s reasoning step
  • Message-ID: the unique identifier of the inbound message
  • In-Reply-To: the strongest signal for what this message answers
  • References: the thread chain when available

Here is a simple payload-mapping shape using Jackson:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class InboundParser {
    private static final ObjectMapper mapper = new ObjectMapper();

    public static void parse(String payload) throws Exception {
        JsonNode root = mapper.readTree(payload);

        String from = root.path("from").asText();
        String subject = root.path("subject").asText();
        String text = root.path("text").asText();
        String inReplyTo = root.path("headers").path("In-Reply-To").asText();
        String references = root.path("headers").path("References").asText();

        System.out.println("from = " + from);
        System.out.println("subject = " + subject);
        System.out.println("text = " + text);
        System.out.println("inReplyTo = " + inReplyTo);
        System.out.println("references = " + references);
    }
}

The rule that keeps threads sane

Do not use the subject line as your primary threading mechanism.

Users edit subjects. Forward messages. Strip prefixes. Mobile clients mangle formatting. Header-based correlation is more reliable. Store your outbound message metadata, then resolve replies against those stored identifiers.

For agents, that means every outbound message should leave your system with a traceable internal record. Then the inbound event can map back to the original task, ticket, or memory item cleanly.

Advanced Email Techniques for Autonomous Agents

Once send and receive work, the next failures are usually around attachments, thread continuity, and observability.

These are not edge cases. They are the difference between a demo and a system that survives real users.

Handle attachments without bloating the flow

Attachments look simple until agents start passing around generated files, reports, or screenshots.

The clean pattern is to keep large file handling separate from the conversational step. Upload the file through a secure path, attach a reference when appropriate, and avoid turning every message send into a giant in-memory payload operation.

For agent systems, this has two benefits:

  • Your email step stays lightweight
  • Your workflow can reason about files as durable artifacts, not transient blobs

If your provider supports secure uploads and presigned URLs, that is usually the more maintainable path than pushing oversized attachments through every mail call.

Treat threading as data, not formatting

A lot of developers still try to preserve conversations by keeping the subject line stable and quoting prior messages. That helps the human reader. It is not enough for the system.

The better model is simple:

  1. Persist outbound message identifiers.
  2. Store any provider-side thread metadata.
  3. On inbound messages, resolve against In-Reply-To and References.
  4. Fall back to subject only when headers are missing.

That approach keeps the agent’s memory grounded in actual message relationships instead of best guesses.

A professional email agent does not “infer” thread continuity from the subject unless it has to.

Use headers intentionally

Custom headers are one of the easiest ways to make an email workflow debuggable.

You can use them to carry:

  • internal task IDs
  • tenant or workspace context
  • workflow names
  • correlation identifiers for logs and traces

Do not overload headers with sensitive data. Use them as pointers, not as storage.

A useful mental model is this: email headers should help your systems find the right record, not contain the whole record.

Design for failure paths

Autonomous email workflows fail in ways normal web requests do not.

Messages can bounce. Replies can arrive days later. A human can reply from a different alias. Attachments can be removed or reformatted by clients. Quoted text can swamp the actual answer.

That means your Java workflow should separate these concerns:

  • delivery acceptance
  • inbound event authenticity
  • message-to-thread correlation
  • agent interpretation of the useful reply fragment

Keep those as distinct steps and your system stays debuggable.

Making the Right Choice for Your Java Project

If your app only needs to send occasional internal notifications, Jakarta Mail is still a reasonable choice. It is familiar, works with standard SMTP infrastructure, and fits a narrow outbound use case.

If your system needs reliable two-way conversations, it is the wrong abstraction.

That is the dividing line.

Use Jakarta Mail when

  • Your scope is outbound only: alerts, receipts, simple notifications
  • You already have SMTP infrastructure: and the operational burden is acceptable
  • You do not need mailbox automation: no reply parsing, no thread state, no autonomous actions

Use an API-first model when

  • Your software is agent-driven: the system has to act on replies
  • You need send and receive together: not just message dispatch
  • You want less protocol plumbing in Java: and more application-level control
  • You care about structured events: for orchestration, retries, and logging

The mistake is not using SMTP. The mistake is using SMTP for a problem that is really about programmatic communication loops.

For older backend applications, the classic approach still has a place.

For AI agents, the sustainable path is the one that treats email as an application primitive, not a mail-server ritual.

Frequently Asked Questions About Sending Email in Java

Should I use Gmail directly for agent workflows

Usually no. Gmail-style setups add account-level friction, especially around security controls and authentication. That is manageable for a human-operated inbox. It is awkward for autonomous systems.

Who handles deliverability in SMTP versus API models

With SMTP, your team usually owns more of the setup and hygiene burden. That includes correct authentication-related configuration and avoiding the mistakes that hurt inbox placement. In an API model, more of that operational layer is abstracted by the provider, though you still own sending quality.

Is there a free way to test an agent-native mailbox flow

Yes. Robotomail’s published free tier includes one mailbox, 50 sends/day, and 1,000 monthly sends, which is enough to prototype an autonomous email loop before committing to a larger setup.

Can Java still be a good choice for email-enabled agents

Absolutely. Java is strong for workflow orchestration, background jobs, and service reliability. The main decision is not language. It is whether you pick an email integration model that matches two-way automation.


If you are building autonomous email workflows in Java, Robotomail is worth a look. It is purpose-built for AI agents, supports real mailboxes instead of outbound-only sending, and gives you send, receive, threading, webhooks, polling, and HMAC-signed events without forcing you through SMTP setup or browser-based consent flows.