Introduction
JavaMail API (now part of Jakarta Mail) provides a platform-independent and protocol-independent framework for building mail and messaging applications. It supports SMTP, POP3, and IMAP protocols for sending and receiving emails.
Maven Dependencies
<!-- Jakarta Mail API --> <dependency> <groupId>com.sun.mail</groupId> <artifactId>jakarta.mail</artifactId> <version>2.0.1</version> </dependency> <!-- For Spring Boot projects --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <!-- For testing --> <dependency> <groupId>com.icegreen</groupId> <artifactId>greenmail</artifactId> <version>1.6.11</version> <scope>test</scope> </dependency>
Basic Email Setup
Simple Email Sending
import jakarta.mail.*; import jakarta.mail.internet.*; import java.util.Properties; public class BasicEmailSender { private final String host; private final int port; private final String username; private final String password; private final boolean auth; private final boolean starttls; public BasicEmailSender(String host, int port, String username, String password, boolean auth, boolean starttls) { this.host = host; this.port = port; this.username = username; this.password = password; this.auth = auth; this.starttls = starttls; } public void sendSimpleEmail(String to, String subject, String content) throws MessagingException { // Setup mail server properties Properties props = new Properties(); props.put("mail.smtp.host", host); props.put("mail.smtp.port", port); props.put("mail.smtp.auth", String.valueOf(auth)); props.put("mail.smtp.starttls.enable", String.valueOf(starttls)); // Create session with authentication Session session = Session.getInstance(props, new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); try { // Create message Message message = new MimeMessage(session); message.setFrom(new InternetAddress(username)); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); message.setSubject(subject); message.setText(content); // Send message Transport.send(message); System.out.println("Email sent successfully to: " + to); } catch (MessagingException e) { System.err.println("Failed to send email: " + e.getMessage()); throw e; } } // Usage example public static void main(String[] args) { BasicEmailSender sender = new BasicEmailSender( "smtp.gmail.com", 587, "[email protected]", "your-app-password", true, true); try { sender.sendSimpleEmail( "[email protected]", "Test Email Subject", "This is a test email content." ); } catch (MessagingException e) { e.printStackTrace(); } } } Advanced Email Features
HTML Email with Attachments
public class AdvancedEmailSender { private Session session; public AdvancedEmailSender(String host, int port, String username, String password, boolean useTLS) { Properties props = new Properties(); props.put("mail.smtp.host", host); props.put("mail.smtp.port", port); props.put("mail.smtp.auth", "true"); props.put("mail.smtp.starttls.enable", String.valueOf(useTLS)); this.session = Session.getInstance(props, new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); // Enable debug mode for troubleshooting session.setDebug(true); } public void sendHtmlEmail(String to, String subject, String htmlContent) throws MessagingException { Message message = new MimeMessage(session); message.setFrom(new InternetAddress(session.getProperty("mail.smtp.user"))); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); message.setSubject(subject); // Set HTML content message.setContent(htmlContent, "text/html; charset=utf-8"); Transport.send(message); } public void sendEmailWithAttachment(String to, String subject, String content, File attachment) throws MessagingException { Message message = new MimeMessage(session); message.setFrom(new InternetAddress(session.getProperty("mail.smtp.user"))); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); message.setSubject(subject); // Create multipart message Multipart multipart = new MimeMultipart(); // Text part BodyPart messageBodyPart = new MimeBodyPart(); messageBodyPart.setText(content); multipart.addBodyPart(messageBodyPart); // Attachment part messageBodyPart = new MimeBodyPart(); DataSource source = new FileDataSource(attachment); messageBodyPart.setDataHandler(new DataHandler(source)); messageBodyPart.setFileName(attachment.getName()); multipart.addBodyPart(messageBodyPart); message.setContent(multipart); Transport.send(message); } public void sendEmailWithMultipleAttachments(String to, String subject, String content, List<File> attachments) throws MessagingException { Message message = new MimeMessage(session); message.setFrom(new InternetAddress(session.getProperty("mail.smtp.user"))); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); message.setSubject(subject); Multipart multipart = new MimeMultipart(); // Text part MimeBodyPart textPart = new MimeBodyPart(); textPart.setText(content); multipart.addBodyPart(textPart); // Add all attachments for (File file : attachments) { MimeBodyPart attachmentPart = new MimeBodyPart(); attachmentPart.attachFile(file); multipart.addBodyPart(attachmentPart); } message.setContent(multipart); Transport.send(message); } public void sendEmailWithInlineImages(String to, String subject, String htmlContent, Map<String, File> inlineImages) throws MessagingException { Message message = new MimeMessage(session); message.setFrom(new InternetAddress(session.getProperty("mail.smtp.user"))); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); message.setSubject(subject); Multipart multipart = new MimeMultipart("related"); // HTML part BodyPart htmlPart = new MimeBodyPart(); htmlPart.setContent(htmlContent, "text/html; charset=utf-8"); multipart.addBodyPart(htmlPart); // Inline images for (Map.Entry<String, File> entry : inlineImages.entrySet()) { BodyPart imagePart = new MimeBodyPart(); imagePart.setDataHandler(new DataHandler(new FileDataSource(entry.getValue()))); imagePart.setHeader("Content-ID", "<" + entry.getKey() + ">"); imagePart.setDisposition(MimeBodyPart.INLINE); multipart.addBodyPart(imagePart); } message.setContent(multipart); Transport.send(message); } } Email Templates and Builder Pattern
Email Builder for Complex Emails
public class EmailBuilder { private final Session session; private String from; private String[] to; private String[] cc; private String[] bcc; private String subject; private String textContent; private String htmlContent; private List<File> attachments; private Map<String, File> inlineImages; private Priority priority = Priority.NORMAL; public enum Priority { HIGH("1"), NORMAL("3"), LOW("5"); private final String value; Priority(String value) { this.value = value; } public String getValue() { return value; } } public EmailBuilder(Session session) { this.session = session; this.attachments = new ArrayList<>(); this.inlineImages = new HashMap<>(); } public EmailBuilder from(String from) { this.from = from; return this; } public EmailBuilder to(String... to) { this.to = to; return this; } public EmailBuilder cc(String... cc) { this.cc = cc; return this; } public EmailBuilder bcc(String... bcc) { this.bcc = bcc; return this; } public EmailBuilder subject(String subject) { this.subject = subject; return this; } public EmailBuilder textContent(String textContent) { this.textContent = textContent; return this; } public EmailBuilder htmlContent(String htmlContent) { this.htmlContent = htmlContent; return this; } public EmailBuilder addAttachment(File file) { this.attachments.add(file); return this; } public EmailBuilder addInlineImage(String contentId, File image) { this.inlineImages.put(contentId, image); return this; } public EmailBuilder priority(Priority priority) { this.priority = priority; return this; } public void send() throws MessagingException { Message message = new MimeMessage(session); // Set basic headers if (from != null) { message.setFrom(new InternetAddress(from)); } message.setRecipients(Message.RecipientType.TO, parseAddresses(to)); if (cc != null && cc.length > 0) { message.setRecipients(Message.RecipientType.CC, parseAddresses(cc)); } if (bcc != null && bcc.length > 0) { message.setRecipients(Message.RecipientType.BCC, parseAddresses(bcc)); } message.setSubject(subject); message.setHeader("X-Priority", priority.getValue()); message.setSentDate(new Date()); // Build message content message.setContent(buildContent()); Transport.send(message); } private Address[] parseAddresses(String[] addresses) throws AddressException { if (addresses == null) return new Address[0]; List<Address> addressList = new ArrayList<>(); for (String address : addresses) { addressList.add(new InternetAddress(address)); } return addressList.toArray(new Address[0]); } private Multipart buildContent() throws MessagingException { Multipart multipart = new MimeMultipart(); // Handle text and HTML content if (textContent != null || htmlContent != null) { MimeBodyPart contentPart = new MimeBodyPart(); if (htmlContent != null && textContent != null) { // Both text and HTML - create alternative content Multipart alternative = new MimeMultipart("alternative"); MimeBodyPart textPart = new MimeBodyPart(); textPart.setText(textContent); alternative.addBodyPart(textPart); MimeBodyPart htmlPart = new MimeBodyPart(); htmlPart.setContent(htmlContent, "text/html; charset=utf-8"); alternative.addBodyPart(htmlPart); contentPart.setContent(alternative); } else if (htmlContent != null) { contentPart.setContent(htmlContent, "text/html; charset=utf-8"); } else { contentPart.setText(textContent); } multipart.addBodyPart(contentPart); } // Add attachments for (File attachment : attachments) { MimeBodyPart attachmentPart = new MimeBodyPart(); attachmentPart.attachFile(attachment); multipart.addBodyPart(attachmentPart); } // Add inline images if (!inlineImages.isEmpty()) { for (Map.Entry<String, File> entry : inlineImages.entrySet()) { MimeBodyPart imagePart = new MimeBodyPart(); imagePart.setDataHandler(new DataHandler(new FileDataSource(entry.getValue()))); imagePart.setHeader("Content-ID", "<" + entry.getKey() + ">"); imagePart.setDisposition(MimeBodyPart.INLINE); multipart.addBodyPart(imagePart); } } return multipart; } } Spring Boot Integration
Spring Boot Mail Configuration
@Configuration @ConfigurationProperties(prefix = "spring.mail") @Data public class MailConfig { private String host; private int port; private String username; private String password; private String protocol = "smtp"; private Properties properties = new Properties(); @Bean public JavaMailSender javaMailSender() { JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); mailSender.setHost(host); mailSender.setPort(port); mailSender.setUsername(username); mailSender.setPassword(password); mailSender.setProtocol(protocol); mailSender.setJavaMailProperties(properties); return mailSender; } } // application.yml spring: mail: host: smtp.gmail.com port: 587 username: [email protected] password: your-app-password protocol: smtp properties: mail: smtp: auth: true starttls: enable: true connectiontimeout: 5000 timeout: 5000 writetimeout: 5000 Spring Service for Email Operations
@Service @Slf4j public class EmailService { private final JavaMailSender mailSender; private final TemplateEngine templateEngine; // Thymeleaf or FreeMarker public EmailService(JavaMailSender mailSender, TemplateEngine templateEngine) { this.mailSender = mailSender; this.templateEngine = templateEngine; } public void sendSimpleMessage(String to, String subject, String text) { SimpleMailMessage message = new SimpleMailMessage(); message.setTo(to); message.setSubject(subject); message.setText(text); message.setFrom(mailSender.getUsername()); try { mailSender.send(message); log.info("Email sent successfully to: {}", to); } catch (MailException e) { log.error("Failed to send email to: {}", to, e); throw new EmailException("Failed to send email", e); } } public void sendMimeMessage(String to, String subject, String htmlContent) { MimeMessage message = mailSender.createMimeMessage(); try { MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); helper.setTo(to); helper.setSubject(subject); helper.setText(htmlContent, true); helper.setFrom(mailSender.getUsername()); mailSender.send(message); log.info("HTML email sent successfully to: {}", to); } catch (MessagingException e) { log.error("Failed to send MIME email to: {}", to, e); throw new EmailException("Failed to send MIME email", e); } } public void sendTemplatedEmail(String to, String subject, String templateName, Map<String, Object> variables) { try { // Process template String htmlContent = templateEngine.process(templateName, new Context(Locale.getDefault(), variables)); sendMimeMessage(to, subject, htmlContent); } catch (Exception e) { log.error("Failed to send templated email to: {}", to, e); throw new EmailException("Failed to send templated email", e); } } public void sendMessageWithAttachment(String to, String subject, String text, String attachmentPath) { MimeMessage message = mailSender.createMimeMessage(); try { MimeMessageHelper helper = new MimeMessageHelper(message, true); helper.setTo(to); helper.setSubject(subject); helper.setText(text); helper.setFrom(mailSender.getUsername()); // Add attachment FileSystemResource file = new FileSystemResource(new File(attachmentPath)); helper.addAttachment(Objects.requireNonNull(file.getFilename()), file); mailSender.send(message); log.info("Email with attachment sent to: {}", to); } catch (MessagingException e) { log.error("Failed to send email with attachment to: {}", to, e); throw new EmailException("Failed to send email with attachment", e); } } public void sendBulkEmails(List<String> recipients, String subject, String templateName, Map<String, Object> variables) { for (String recipient : recipients) { try { sendTemplatedEmail(recipient, subject, templateName, variables); // Small delay to avoid being flagged as spam Thread.sleep(100); } catch (Exception e) { log.error("Failed to send bulk email to: {}", recipient, e); // Continue with other emails even if one fails } } } } // Custom exception class EmailException extends RuntimeException { public EmailException(String message) { super(message); } public EmailException(String message, Throwable cause) { super(message, cause); } } Asynchronous Email Sending
Async Email Service
@Service @Slf4j public class AsyncEmailService { private final JavaMailSender mailSender; private final TaskExecutor taskExecutor; public AsyncEmailService(JavaMailSender mailSender, @Qualifier("taskExecutor") TaskExecutor taskExecutor) { this.mailSender = mailSender; this.taskExecutor = taskExecutor; } @Async public CompletableFuture<Void> sendEmailAsync(String to, String subject, String text) { return CompletableFuture.runAsync(() -> { try { SimpleMailMessage message = new SimpleMailMessage(); message.setTo(to); message.setSubject(subject); message.setText(text); message.setFrom(mailSender.getUsername()); mailSender.send(message); log.info("Async email sent successfully to: {}", to); } catch (Exception e) { log.error("Failed to send async email to: {}", to, e); throw new EmailException("Failed to send async email", e); } }, taskExecutor); } public void sendEmailWithCallback(String to, String subject, String text) { taskExecutor.execute(() -> { try { SimpleMailMessage message = new SimpleMailMessage(); message.setTo(to); message.setSubject(subject); message.setText(text); message.setFrom(mailSender.getUsername()); mailSender.send(message); log.info("Callback email sent successfully to: {}", to); } catch (Exception e) { log.error("Failed to send callback email to: {}", to, e); } }); } } // Configuration for async support @Configuration @EnableAsync public class AsyncConfig { @Bean("taskExecutor") public TaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(25); executor.setThreadNamePrefix("EmailExecutor-"); executor.initialize(); return executor; } } Email Validation and Utilities
Email Validation Service
@Service public class EmailValidationService { private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE); public boolean isValidEmail(String email) { if (email == null || email.trim().isEmpty()) { return false; } Matcher matcher = EMAIL_PATTERN.matcher(email); return matcher.matches(); } public List<String> validateEmailList(List<String> emails) { return emails.stream() .filter(this::isValidEmail) .collect(Collectors.toList()); } public void validateEmailOrThrow(String email) { if (!isValidEmail(email)) { throw new IllegalArgumentException("Invalid email address: " + email); } } } // Email utility class @Component public class EmailUtils { public InternetAddress[] parseAddressList(String addresses) throws AddressException { return InternetAddress.parse(addresses); } public String generateMessageId(String domain) { return "<" + UUID.randomUUID() + "@" + domain + ">"; } public Map<String, String> parseEmailHeaders(Message message) throws MessagingException { Map<String, String> headers = new HashMap<>(); Enumeration<Header> allHeaders = message.getAllHeaders(); while (allHeaders.hasMoreElements()) { Header header = allHeaders.nextElement(); headers.put(header.getName(), header.getValue()); } return headers; } public void addCustomHeaders(Message message, Map<String, String> headers) throws MessagingException { for (Map.Entry<String, String> entry : headers.entrySet()) { message.addHeader(entry.getKey(), entry.getValue()); } } } Testing Email Functionality
Test Configuration with GreenMail
@SpringBootTest @TestPropertySource(properties = { "spring.mail.host=127.0.0.1", "spring.mail.port=3025", "spring.mail.username=test@localhost", "spring.mail.password=test" }) public class EmailServiceTest { @Autowired private EmailService emailService; @RegisterExtension static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP) .withConfiguration(GreenMailConfiguration.aConfig().withUser("test", "test")) .withPerMethodLifecycle(false); @Test public void testSendSimpleEmail() throws Exception { // Given String to = "[email protected]"; String subject = "Test Subject"; String text = "Test Content"; // When emailService.sendSimpleMessage(to, subject, text); // Then await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> { MimeMessage[] receivedMessages = greenMail.getReceivedMessages(); assertThat(receivedMessages).hasSize(1); MimeMessage receivedMessage = receivedMessages[0]; assertThat(receivedMessage.getSubject()).isEqualTo(subject); assertThat(receivedMessage.getAllRecipients()[0].toString()).isEqualTo(to); }); } @Test public void testSendEmailWithAttachment() throws Exception { // Given String to = "[email protected]"; String subject = "Test Attachment"; String text = "Test Content with Attachment"; String attachmentPath = "test-file.txt"; // Create test file Files.write(Paths.get(attachmentPath), "Test attachment content".getBytes()); // When emailService.sendMessageWithAttachment(to, subject, text, attachmentPath); // Then await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> { MimeMessage[] receivedMessages = greenMail.getReceivedMessages(); assertThat(receivedMessages).hasSize(1); MimeMessage receivedMessage = receivedMessages[0]; assertThat(receivedMessage.getSubject()).isEqualTo(subject); // Verify attachment assertThat(receivedMessage.getContent()).isInstanceOf(MimeMultipart.class); MimeMultipart multipart = (MimeMultipart) receivedMessage.getContent(); assertThat(multipart.getCount()).isGreaterThan(1); }); // Cleanup Files.deleteIfExists(Paths.get(attachmentPath)); } } // Unit test for validation @Test public void testEmailValidation() { EmailValidationService validator = new EmailValidationService(); assertTrue(validator.isValidEmail("[email protected]")); assertTrue(validator.isValidEmail("[email protected]")); assertFalse(validator.isValidEmail("invalid-email")); assertFalse(validator.isValidEmail("")); assertFalse(validator.isValidEmail(null)); } Best Practices and Production Considerations
Production-Ready Email Service
@Service @Slf4j public class ProductionEmailService { private final JavaMailSender mailSender; private final EmailValidationService validationService; private final RetryTemplate retryTemplate; public ProductionEmailService(JavaMailSender mailSender, EmailValidationService validationService) { this.mailSender = mailSender; this.validationService = validationService; this.retryTemplate = createRetryTemplate(); } public EmailResult sendEmailWithRetry(EmailRequest request) { try { validationService.validateEmailOrThrow(request.getTo()); return retryTemplate.execute(context -> { try { sendEmailInternal(request); return EmailResult.success(request.getTo()); } catch (Exception e) { log.warn("Attempt {} failed for email to: {}", context.getRetryCount(), request.getTo(), e); if (context.getRetryCount() >= 2) { return EmailResult.failure(request.getTo(), e.getMessage()); } throw e; } }); } catch (Exception e) { log.error("Failed to send email after retries to: {}", request.getTo(), e); return EmailResult.failure(request.getTo(), e.getMessage()); } } private void sendEmailInternal(EmailRequest request) throws MessagingException { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); helper.setTo(request.getTo()); helper.setSubject(request.getSubject()); helper.setText(request.getContent(), request.isHtml()); helper.setFrom(mailSender.getUsername()); if (request.getCc() != null) { helper.setCc(request.getCc()); } if (request.getBcc() != null) { helper.setBcc(request.getBcc()); } // Add attachments if (request.getAttachments() != null) { for (EmailAttachment attachment : request.getAttachments()) { helper.addAttachment(attachment.getFilename(), new ByteArrayResource(attachment.getContent())); } } mailSender.send(message); } private RetryTemplate createRetryTemplate() { RetryTemplate template = new RetryTemplate(); ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval(1000); backOffPolicy.setMultiplier(2.0); backOffPolicy.setMaxInterval(10000); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); retryPolicy.setMaxAttempts(3); template.setBackOffPolicy(backOffPolicy); template.setRetryPolicy(retryPolicy); return template; } // DTO classes @Data public static class EmailRequest { private String to; private String subject; private String content; private boolean html = false; private String[] cc; private String[] bcc; private List<EmailAttachment> attachments; } @Data public static class EmailAttachment { private String filename; private byte[] content; private String contentType; } @Data public static class EmailResult { private final String recipient; private final boolean success; private final String errorMessage; private final Instant timestamp; public static EmailResult success(String recipient) { return new EmailResult(recipient, true, null, Instant.now()); } public static EmailResult failure(String recipient, String errorMessage) { return new EmailResult(recipient, false, errorMessage, Instant.now()); } } } Conclusion
JavaMail API provides comprehensive email functionality for Java applications. Key points to remember:
- Use Jakarta Mail for new projects (the successor to JavaMail)
- Configure proper SMTP settings including authentication and TLS
- Handle attachments and HTML content appropriately
- Implement error handling and retry mechanisms for production
- Use Spring Boot's mail support for easier configuration
- Test email functionality with tools like GreenMail
- Follow email best practices to avoid being flagged as spam
By following these patterns and best practices, you can build robust, production-ready email functionality in your Java applications.