Groundmail

Blog

How to send email in Java and Spring Boot

Send email in Java with Jakarta Mail (formerly JavaMail), in Spring Boot with spring-boot-starter-mail and JavaMailSender, or with an HTTP email API. Includes HTML, attachments, common errors and production tips.

By Groundmail4 min read

On this page

To send email in Java, use Jakarta Mail (the API formerly called JavaMail): set the SMTP host, port 587 and STARTTLS in a Properties object, create a Session with your credentials, build a MimeMessage, and call Transport.send(). In Spring Boot, spring-boot-starter-mail does the setup from application.properties and gives you a JavaMailSender bean. For application email at volume, an HTTP email API called with java.net.http.HttpClient is often simpler: one authenticated request per message, with a message ID and structured errors back.

All three are below, with HTML, attachments, the errors you'll most likely hit, and what to change before production.

What you need

  • Java 17 or later (Spring Boot 3 and 4 require it)
  • An SMTP server or email API to send through, with credentials
  • A sending domain you control, with SPF, DKIM and DMARC set up, so your mail isn't filtered as spam

Keep credentials in environment variables or a secret manager, never in code or in a properties file committed to git:

export SMTP_HOST=smtp.yourprovider.example
export SMTP_USER=your-username
export SMTP_PASSWORD=your-password

JavaMail or Jakarta Mail?

They are the same API. JavaMail moved to the Eclipse Foundation and became Jakarta Mail, and from version 2.0 the package changed from javax.mail to jakarta.mail. Most tutorials still show javax.mail with the old com.sun.mail:javax.mail artifact. For new code, use Jakarta Mail with the Eclipse Angus Mail implementation:

<dependency>
  <groupId>org.eclipse.angus</groupId>
  <artifactId>angus-mail</artifactId>
  <version>2.0.5</version>
</dependency>

Don't mix the two on one classpath: javax.mail classes can't be passed to jakarta.mail code, and the errors are confusing (NoClassDefFoundError, NoSuchProviderException: smtp).

Option 1: Jakarta Mail

This sends an email with an HTML body, a plain-text fallback and a PDF attachment:

import jakarta.activation.DataHandler;
import jakarta.activation.FileDataSource;
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.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import java.io.UnsupportedEncodingException;
import java.util.Properties;

public class SendReceipt {

    public static void main(String[] args) throws MessagingException, UnsupportedEncodingException {
        Properties props = new Properties();
        props.put("mail.smtp.host", System.getenv("SMTP_HOST"));
        props.put("mail.smtp.port", "587");
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.starttls.enable", "true");
        props.put("mail.smtp.starttls.required", "true");
        props.put("mail.smtp.connectiontimeout", "10000");
        props.put("mail.smtp.timeout", "30000");

        Session session = Session.getInstance(props, new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(System.getenv("SMTP_USER"), System.getenv("SMTP_PASSWORD"));
            }
        });

        // Text and HTML versions of the same content
        MimeBodyPart text = new MimeBodyPart();
        text.setText("Thanks for your order. Your invoice is attached.", "UTF-8");
        MimeBodyPart html = new MimeBodyPart();
        html.setContent("<p>Thanks for your order. Your invoice is attached.</p>", "text/html; charset=UTF-8");
        MimeBodyPart content = new MimeBodyPart();
        content.setContent(new MimeMultipart("alternative", text, html));

        // The attachment
        MimeBodyPart invoice = new MimeBodyPart();
        invoice.setDataHandler(new DataHandler(new FileDataSource("invoice-1042.pdf")));
        invoice.setFileName("invoice-1042.pdf");

        MimeMessage message = new MimeMessage(session);
        message.setFrom(new InternetAddress("billing@yourdomain.com", "Acme"));
        message.setRecipients(Message.RecipientType.TO, InternetAddress.parse("user@example.com"));
        message.setSubject("Your receipt", "UTF-8");
        message.setContent(new MimeMultipart("mixed", content, invoice));

        Transport.send(message);
    }
}

What each part does:

  • mail.smtp.starttls.enable upgrades the connection to TLS before logging in, and mail.smtp.starttls.required refuses to continue if the server can't, instead of silently sending your password in plain text. For implicit TLS on port 465, set mail.smtp.ssl.enable to true instead.
  • The two timeouts matter: Jakarta Mail's defaults are infinite, so a blocked port hangs a thread forever.
  • The alternative multipart holds the text and HTML versions; the mixed multipart wraps it with the attachment.
  • Transport.send() opens a connection, sends and closes. To send many messages, get a Transport from session.getTransport("smtp"), connect() once and call sendMessage() for each, which is much faster.

Option 2: Spring Boot with spring-boot-starter-mail

Spring Boot configures Jakarta Mail for you. Add the starter:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

Set the spring.mail.* properties:

spring.mail.host=${SMTP_HOST}
spring.mail.port=587
spring.mail.username=${SMTP_USER}
spring.mail.password=${SMTP_PASSWORD}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.connectiontimeout=10000
spring.mail.properties.mail.smtp.timeout=30000

Then inject the JavaMailSender bean:

import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

@Service
public class ReceiptMailer {

    private final JavaMailSender mailSender;

    public ReceiptMailer(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

    public void sendReceipt(String to, String orderId, byte[] invoicePdf) throws MessagingException {
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); // true = multipart

        helper.setFrom("billing@yourdomain.com");
        helper.setTo(to);
        helper.setSubject("Receipt for order " + orderId);
        helper.setText("Thanks for order " + orderId + ".", "<p>Thanks for order " + orderId + ".</p>");
        helper.addAttachment("invoice-" + orderId + ".pdf", new ByteArrayResource(invoicePdf), "application/pdf");

        mailSender.send(message);
    }
}

setText(plain, html) builds the text and HTML alternatives for you. For a plain-text email, SimpleMailMessage is shorter: set from, to, subject and text, and pass it to mailSender.send(). Spring wraps failures in MailAuthenticationException and MailSendException.

Option 3: an HTTP email API with HttpClient

With an API you skip the SMTP conversation and post JSON. Here's the same receipt with the Groundmail API and the JDK's own HttpClient, no dependencies:

HttpClient http = HttpClient.newHttpClient();

String json = """
        {
          "from": "Acme <billing@yourdomain.com>",
          "to": ["user@example.com"],
          "subject": "Your receipt",
          "html": "<p>Thanks for your order.</p>",
          "text": "Thanks for your order."
        }
        """;

HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.groundmail.dev/emails"))
        .header("Authorization", "Bearer " + System.getenv("GROUNDMAIL_API_KEY"))
        .header("Content-Type", "application/json")
        .header("Idempotency-Key", "receipt-order-1042")
        .timeout(Duration.ofSeconds(30))
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .build();

HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body()); // 200 {"id":"msg_…","status":"sent"}

In a real app, build the body with Jackson rather than a string so user input is escaped. You get a message ID back that you can store and look up later, and errors come back as JSON with a code you can branch on. The Idempotency-Key header makes retries safe: a repeated request with the same key returns the original response instead of sending a second email.

The Java and Spring Boot guide has a complete client with typed records, error handling and retries, plus a Spring RestClient version.

Common errors and what they mean

jakarta.mail.AuthenticationFailedException: 535 .... The server rejected your username or password. Check for whitespace in environment variables, and that you're using the right credential type: many providers want an SMTP-specific credential or API key, not your account password. Gmail returns 535-5.7.8 Username and Password not accepted unless you use an app password.

SMTPSendFailedException: 530 5.7.0 Must issue a STARTTLS command first. STARTTLS isn't enabled. Set mail.smtp.starttls.enable to true (in Spring Boot, spring.mail.properties.mail.smtp.starttls.enable=true).

MessagingException: Could not convert socket to TLS. The TLS handshake failed. Usually it's a port mismatch (starttls settings on port 465, or ssl.enable on 587), an old JDK, or a server certificate your JVM doesn't trust. Fix the port pairing and update the JDK; don't turn off certificate checks with mail.smtp.ssl.trust=*.

MailConnectException: Couldn't connect to host, port: …, 25; timeout -1. Two problems in one: mail.smtp.port wasn't set, so it used port 25, which cloud providers block, and no timeout is set. Set the port to 587 and add the timeouts.

NoSuchProviderException: smtp or NoClassDefFoundError: javax/mail/.... You have the API without an implementation, or javax.mail and jakarta.mail jars mixed together. Depend on org.eclipse.angus:angus-mail and remove the old javax.mail artifacts.

SMTPSendFailedException: 550 ... or 554 .... The server refused the message. Read the text: a refusal about the sender usually means your From domain isn't verified with the provider.

Production tips

  • Keep credentials in the environment or a secret manager, and use a credential that can only send.
  • Send from a background job (a queue consumer, @Async method or scheduled task), not inside the web request.
  • Retry temporary failures with backoff. Retry 4xx SMTP replies, HTTP 429 and 5xx responses and network errors. Don't retry permanent failures such as 5xx SMTP replies or HTTP 400 and 422: they'll fail the same way.
  • Make retries safe, with an idempotency key derived from what the email is for (an order ID, a reset token ID), so a request that timed out after the provider accepted it doesn't send twice.
  • Send a plain-text part with your HTML. Some clients show only text, and spam filters penalise HTML-only messages.

How Groundmail helps

Groundmail gives you both options on the same account: an SMTP relay at smtp.groundmail.dev on port 587 that works with the spring.mail.* settings above (username groundmail, password an SMTP credential), and a REST API you call with HttpClient or RestClient. Test keys and simulator addresses let you test bounces and complaints without sending real mail. The Java and Spring Boot guide covers both end to end. To check that what your code sends lands in the inbox, send one message to the free email spam test.

Frequently asked questions

What is the difference between JavaMail and Jakarta Mail?

They are the same API under a new name. JavaMail moved to the Eclipse Foundation as Jakarta Mail, and from version 2.0 the package is jakarta.mail instead of javax.mail. New projects should use Jakarta Mail with the Eclipse Angus Mail implementation; Spring Boot 3 and later use it too.

How do I send email in Spring Boot?

Add spring-boot-starter-mail, set spring.mail.host, port, username and password plus the STARTTLS properties in application.properties, and inject the JavaMailSender bean. Use MimeMessageHelper for HTML and attachments, or SimpleMailMessage for plain text.

Which port should I use for SMTP in Java?

Use port 587 with mail.smtp.starttls.enable and mail.smtp.starttls.required set to true, or port 465 with mail.smtp.ssl.enable if your provider offers implicit TLS. Don't use port 25 from cloud hosts; it's usually blocked.

Why does my Java email hang or time out?

Jakarta Mail waits forever by default. Set mail.smtp.connectiontimeout and mail.smtp.timeout (in milliseconds), and check that the host and port are right and that your network allows outbound connections on that port.

Send from infrastructure we own.

Groundmail runs its own mail servers on its own IP space. Start free on the shared transactional pool, then move to dedicated IPs or a dedicated server without changing your code.