{"id":432,"date":"2026-09-22T11:10:26","date_gmt":"2026-09-22T16:40:26","guid":{"rendered":"https:\/\/photonconsole.com\/blog\/?p=432"},"modified":"2026-09-22T11:10:27","modified_gmt":"2026-09-22T16:40:27","slug":"spring-boot-email-javamailsender-configuration-for-production","status":"publish","type":"post","link":"https:\/\/photonconsole.com\/blog\/spring-boot-email-javamailsender-configuration-for-production\/","title":{"rendered":"Spring Boot Email: JavaMailSender Configuration for Production"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Spring Boot makes email look almost free. Add one starter dependency, set a few properties, inject <code>JavaMailSender<\/code>, and a test message goes out on the first try.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Production is where the gaps show. A request thread hangs for minutes because a mail server stopped responding. The application refuses to start because a bean could not be created. An <code>@Async<\/code> method runs synchronously for reasons nobody can see. Or everything succeeds and the message lands in spam anyway.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers the production setup: the properties that matter, the timeout default that causes most outages, HTML templates with Thymeleaf, asynchronous sending that actually works asynchronously, and the relay configuration underneath it all.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Quick Answer: How Do You Send Email in Spring Boot?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Add the mail starter, configure SMTP in <code>application.properties<\/code>, and inject <code>JavaMailSender<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;dependency&gt;\n    &lt;groupId&gt;org.springframework.boot&lt;\/groupId&gt;\n    &lt;artifactId&gt;spring-boot-starter-mail&lt;\/artifactId&gt;\n&lt;\/dependency&gt;<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>spring.mail.host=smtp.photonrelay.com\nspring.mail.port=587\nspring.mail.username=${SMTP_USER}\nspring.mail.password=${SMTP_PASS}\nspring.mail.properties.mail.smtp.auth=true\nspring.mail.properties.mail.smtp.starttls.enable=true\nspring.mail.properties.mail.smtp.starttls.required=true\nspring.mail.properties.mail.smtp.connectiontimeout=5000\nspring.mail.properties.mail.smtp.timeout=5000\nspring.mail.properties.mail.smtp.writetimeout=5000<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>@Service\npublic class EmailService {\n\n    private final JavaMailSender mailSender;\n\n    public EmailService(JavaMailSender mailSender) {\n        this.mailSender = mailSender;\n    }\n\n    public void sendCode(String to, String code) {\n        SimpleMailMessage message = new SimpleMailMessage();\n        message.setFrom(\"noreply@yourdomain.com\");\n        message.setTo(to);\n        message.setSubject(\"Your verification code\");\n        message.setText(\"Your code is \" + code + \".\");\n        mailSender.send(message);\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The three timeout lines are not optional, and the next sections explain why. Delivery itself depends on the server you authenticate against and whether your domain is authorised to send \u2014 an authenticated relay such as <a href=\"https:\/\/www.photonconsole.com\/\">PhotonConsole<\/a> covers that side.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How Spring Boot Mail Works<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/spring.io\/projects\/spring-boot\" target=\"_blank\" rel=\"noopener\">Spring Boot<\/a> auto-configures a <code>JavaMailSenderImpl<\/code> bean when it finds <code>spring.mail.host<\/code> in your configuration. That bean wraps <a href=\"https:\/\/jakarta.ee\/specifications\/mail\/\" target=\"_blank\" rel=\"noopener\">Jakarta Mail<\/a>, which opens the SMTP connection and speaks the protocol.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two consequences follow. The bean only exists if the host property is present. And every connection setting Jakarta Mail understands \u2014 timeouts, TLS, authentication \u2014 is passed through the <code>spring.mail.properties.*<\/code> namespace, not through dedicated Spring properties.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Note<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Spring Boot 3 moved from <code>javax.mail<\/code> to <code>jakarta.mail<\/code>. Code copied from older tutorials that imports <code>javax.mail.internet.MimeMessage<\/code> will not compile on Spring Boot 3. Change the imports to the <code>jakarta.mail<\/code> package \u2014 the class names are otherwise identical.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why Spring Boot Email Fails in Production<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img fetchpriority=\"high\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-22-2026-04_29_29-PM-1024x576.png\" alt=\"Diagram of four Spring Boot email failure points: infinite default timeout, missing JavaMailSender bean, STARTTLS not enabled and @Async self-invocation\" class=\"wp-image-434\" style=\"aspect-ratio:1.7777777777777777;width:1200px;height:auto\" srcset=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-22-2026-04_29_29-PM-1024x576.png 1024w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-22-2026-04_29_29-PM-300x169.png 300w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-22-2026-04_29_29-PM-768x432.png 768w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-22-2026-04_29_29-PM-1536x864.png 1536w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-22-2026-04_29_29-PM.png 1672w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Four configuration gaps that cause most Spring Boot email incidents.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Most failures come from configuration rather than defects in Spring. These five account for the majority.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. No Timeouts Are Set<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Jakarta Mail&#8217;s default connection, read and write timeouts are effectively infinite. If the mail server accepts the connection and then stops responding, the calling thread waits indefinitely. In a web application that thread is a request thread, and enough of them stalled at once takes the whole service down.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the single most important property block in any Spring Boot mail configuration, and most tutorials leave it out.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. JavaMailSender Bean Not Found<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The application fails to start with an error saying no qualifying bean of type <code>JavaMailSender<\/code> is available. The cause is almost always a missing or misspelled <code>spring.mail.host<\/code>, often because it is defined in a profile that is not active in that environment.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. STARTTLS Not Enabled<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Setting the port to 587 does not enable encryption by itself. Without <code>mail.smtp.starttls.enable=true<\/code> the client never upgrades the connection, and most relays reject authentication over a plaintext channel.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. @Async Runs Synchronously<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Annotating a method with <code>@Async<\/code> does nothing unless async support is enabled, and nothing when the method is called from inside the same class. The send appears to work, but it is still blocking the request thread.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5. No Authentication Records on the Sending Domain<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Without SPF and DKIM, receiving servers cannot verify your application is allowed to send as your domain, in line with <a href=\"https:\/\/support.google.com\/mail\/answer\/81126\" target=\"_blank\" rel=\"noopener\">Google&#8217;s sender guidelines<\/a>. Our guide to <a href=\"https:\/\/photonconsole.com\/blog\/spf-dkim-dmarc-explained-simply\/\">SPF, DKIM and DMARC<\/a> covers the records, and the free <a href=\"https:\/\/www.photonconsole.com\/email-deliverability-checker.php\">email deliverability checker<\/a> shows what your domain currently publishes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Common Mistake<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Deploying without the three <code>mail.smtp.*timeout<\/code> properties. Because Jakarta Mail waits indefinitely by default, one unresponsive mail server can hold request threads open until the container&#8217;s thread pool is exhausted. The symptom looks like the whole application freezing, not like an email problem, which is why it is so often misdiagnosed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Quick Fix<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Application Fails to Start or Mail Never Connects<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Confirm <code>spring.mail.host<\/code> exists in the active profile, not only in a dev profile<\/li>\n\n\n\n<li>Add <code>starttls.enable<\/code> and <code>starttls.required<\/code> for port 587<\/li>\n\n\n\n<li>Add connection, read and write timeouts of around 5 seconds<\/li>\n\n\n\n<li>Try port 2525 if your host blocks 587 outbound<\/li>\n\n\n\n<li>Set <code>spring.mail.properties.mail.debug=true<\/code> temporarily to see the SMTP conversation<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Step-by-Step: Production Setup<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Externalise Credentials<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Never commit an SMTP password in <code>application.properties<\/code>. Reference environment variables with placeholders, as shown in the quick answer, and supply the values at runtime.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export SMTP_USER=your_project_api_user\nexport SMTP_PASS=your_secret_api_key<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Send HTML With a Plain Text Alternative<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>SimpleMailMessage<\/code> is plain text only. For HTML, use <code>MimeMessageHelper<\/code> with multipart enabled, and always include a plain text version \u2014 HTML-only messages score worse with spam filters.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import jakarta.mail.MessagingException;\nimport jakarta.mail.internet.MimeMessage;\nimport org.springframework.mail.javamail.MimeMessageHelper;\n\npublic void sendHtml(String to, String subject, String html, String text)\n        throws MessagingException {\n\n    MimeMessage message = mailSender.createMimeMessage();\n    MimeMessageHelper helper = new MimeMessageHelper(message, true, \"UTF-8\");\n\n    helper.setFrom(\"noreply@yourdomain.com\");\n    helper.setTo(to);\n    helper.setSubject(subject);\n    helper.setText(text, html);   \/\/ plain text first, HTML second\n\n    mailSender.send(message);\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Render Templates With Thymeleaf<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Keep email markup out of Java strings. With <code>spring-boot-starter-thymeleaf<\/code> on the classpath, <a href=\"https:\/\/www.thymeleaf.org\/\" target=\"_blank\" rel=\"noopener\">Thymeleaf<\/a> can render email templates the same way it renders pages.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- src\/main\/resources\/templates\/email\/welcome.html --&gt;\n&lt;html xmlns:th=\"http:\/\/www.thymeleaf.org\"&gt;\n&lt;body&gt;\n  &lt;p&gt;Hello &lt;span th:text=\"${name}\"&gt;there&lt;\/span&gt;,&lt;\/p&gt;\n  &lt;p&gt;Your account is ready.&lt;\/p&gt;\n  &lt;a th:href=\"${loginUrl}\"&gt;Sign in&lt;\/a&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>import org.thymeleaf.context.Context;\nimport org.thymeleaf.spring6.SpringTemplateEngine;\n\npublic void sendWelcome(String to, String name) throws MessagingException {\n    Context ctx = new Context();\n    ctx.setVariable(\"name\", name);\n    ctx.setVariable(\"loginUrl\", \"https:\/\/yourapp.com\/login\");\n\n    String html = templateEngine.process(\"email\/welcome\", ctx);\n    String text = \"Hello \" + name + \", your account is ready: https:\/\/yourapp.com\/login\";\n\n    sendHtml(to, \"Welcome to Your App\", html, text);\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4: Send Asynchronously \u2014 Correctly<\/h3>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img decoding=\"async\" width=\"1024\" height=\"577\" src=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-22-2026-04_34_54-PM-1024x577.png\" alt=\"Spring Boot async email flow showing a controller calling a separate @Async bean on a dedicated mail executor that sends through an SMTP relay\" class=\"wp-image-435\" style=\"aspect-ratio:1.7777777777777777;width:1200px;height:auto\" srcset=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-22-2026-04_34_54-PM-1024x577.png 1024w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-22-2026-04_34_54-PM-300x169.png 300w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-22-2026-04_34_54-PM-767x432.png 767w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-22-2026-04_34_54-PM-1536x865.png 1536w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-22-2026-04_34_54-PM.png 1671w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">The async method must live in a separate bean so the call passes through the Spring proxy.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Enable async support once, then annotate the sending method.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@Configuration\n@EnableAsync\npublic class AsyncConfig {\n\n    @Bean(name = \"mailExecutor\")\n    public ThreadPoolTaskExecutor mailExecutor() {\n        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();\n        executor.setCorePoolSize(2);\n        executor.setMaxPoolSize(5);\n        executor.setQueueCapacity(500);\n        executor.setThreadNamePrefix(\"mail-\");\n        executor.initialize();\n        return executor;\n    }\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>@Service\npublic class AsyncEmailService {\n\n    private final EmailService emailService;\n    private static final Logger log = LoggerFactory.getLogger(AsyncEmailService.class);\n\n    public AsyncEmailService(EmailService emailService) {\n        this.emailService = emailService;\n    }\n\n    @Async(\"mailExecutor\")\n    public void sendWelcomeAsync(String to, String name) {\n        try {\n            emailService.sendWelcome(to, name);\n        } catch (Exception e) {\n            log.error(\"Welcome email to {} failed\", to, e);\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The async method lives in a separate bean from the code that calls it. That is deliberate, and it is the part most implementations get wrong.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Quick Fix<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@Async Method Still Blocks the Request<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Confirm <code>@EnableAsync<\/code> is present on a configuration class<\/li>\n\n\n\n<li>Call the async method from a different bean \u2014 calls within the same class bypass the proxy<\/li>\n\n\n\n<li>Make sure the method is <code>public<\/code>; private methods cannot be proxied<\/li>\n\n\n\n<li>Define a dedicated executor so mail does not compete with other async work<\/li>\n\n\n\n<li>Log the thread name inside the method \u2014 if it is not <code>mail-*<\/code>, it is not running async<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Step 5: Handle Failures and Retries<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Exceptions thrown inside an <code>@Async<\/code> void method are not propagated to the caller, so they must be logged inside the method as shown above. For retries, Spring Retry provides <code>@Retryable<\/code>, but retry only transient failures \u2014 a permanent rejection retried repeatedly damages sender reputation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For mail that must survive an application restart, such as receipts or password resets, an in-memory executor queue is not enough. Our guide to <a href=\"https:\/\/photonconsole.com\/blog\/transactional-email-queue-architecture-explained\/\">transactional email queue architecture<\/a> covers durable alternatives, and <a href=\"https:\/\/photonconsole.com\/blog\/smtp-retry-logic-explained-for-transactional-email-systems\/\">SMTP retry logic<\/a> explains which failures are worth retrying.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Port and Property Reference<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Port<\/th><th>Required properties<\/th><th>When to use<\/th><\/tr><\/thead><tbody><tr><td>587<\/td><td><code>mail.smtp.starttls.enable=true<\/code><\/td><td>Recommended default<\/td><\/tr><tr><td>465<\/td><td><code>mail.smtp.ssl.enable=true<\/code><\/td><td>When the provider requires implicit SSL<\/td><\/tr><tr><td>2525<\/td><td><code>mail.smtp.starttls.enable=true<\/code><\/td><td>When the host blocks 587 and 465<\/td><\/tr><tr><td>25<\/td><td>None<\/td><td>Avoid \u2014 blocked on nearly all hosts<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Timeout Properties Explained<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Property<\/th><th>What it limits<\/th><th>Suggested value<\/th><\/tr><\/thead><tbody><tr><td><code>mail.smtp.connectiontimeout<\/code><\/td><td>Opening the TCP connection<\/td><td>5000 ms<\/td><\/tr><tr><td><code>mail.smtp.timeout<\/code><\/td><td>Waiting for a server response<\/td><td>5000 ms<\/td><\/tr><tr><td><code>mail.smtp.writetimeout<\/code><\/td><td>Writing the message to the socket<\/td><td>5000 ms<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Configuring Spring Boot With PhotonConsole<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Two steps: authenticate your domain in DNS, then set the properties.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>TXT    @                    v=spf1 include:relay.photonconsole.com ~all\nCNAME  photon._domainkey    dkim.photonconsole.com<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>spring.mail.host=smtp.photonrelay.com\nspring.mail.port=587\nspring.mail.username=${SMTP_USER}\nspring.mail.password=${SMTP_PASS}\nspring.mail.properties.mail.smtp.auth=true\nspring.mail.properties.mail.smtp.starttls.enable=true\nspring.mail.properties.mail.smtp.starttls.required=true\nspring.mail.properties.mail.smtp.connectiontimeout=5000\nspring.mail.properties.mail.smtp.timeout=5000\nspring.mail.properties.mail.smtp.writetimeout=5000<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">DNS records can take up to 24-48 hours to propagate, so verify them before assuming a configuration error. Port 2525 exists for hosts that block the standard SMTP ports. Every PhotonConsole account includes 5,000 free emails per month, enough to validate the full setup \u2014 templates, async executor and retry handling \u2014 before any spend. Details are on the <a href=\"https:\/\/www.photonconsole.com\/relay.php\">PhotonRelay page<\/a>, and <a href=\"https:\/\/www.photonconsole.com\/pricing.php\">pricing<\/a> has no monthly minimum.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you are comparing options first, our analysis of <a href=\"https:\/\/photonconsole.com\/blog\/free-smtp-servers\/\">free SMTP servers<\/a> covers where each stops being practical.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Environment-Specific Notes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Spring Boot Actuator<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When Actuator is present, Spring Boot adds a mail health indicator that connects to the SMTP server on every health check. If the relay is briefly unreachable, your whole service reports as unhealthy and an orchestrator may restart it. Disable it with <code>management.health.mail.enabled=false<\/code> if mail is not critical to service health.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Kubernetes and Docker<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Supply credentials through Secrets mapped to environment variables. Containers have no local mail agent, so an external relay is required.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">AWS and Cloud Hosting<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Cloud providers restrict outbound port 25 by default \u2014 AWS documents its <a href=\"https:\/\/repost.aws\/knowledge-center\/ec2-port-25-throttle\" target=\"_blank\" rel=\"noopener\">port 25 throttle removal process<\/a>. Use 587 or 2525 instead of requesting the restriction be lifted.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Tests<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use an embedded SMTP server such as GreenMail in integration tests so no real mail is sent and assertions can inspect the received message.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Pro Tips<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Set all three timeouts.<\/strong> Connection, read and write. Missing any one leaves a path for an indefinite hang.<\/li>\n\n\n\n<li><strong>Give mail its own executor.<\/strong> A dedicated thread pool stops a slow mail server from starving unrelated async work.<\/li>\n\n\n\n<li><strong>Send from your own domain.<\/strong> Put a user&#8217;s address in Reply-To, never From, or SPF and DMARC fail.<\/li>\n\n\n\n<li><strong>Never log the mail properties.<\/strong> They include the password. Log the recipient and exception only.<\/li>\n\n\n\n<li><strong>Verify DNS after any change.<\/strong> <a href=\"https:\/\/mxtoolbox.com\/\" target=\"_blank\" rel=\"noopener\">MXToolbox<\/a> checks SPF, DKIM and blocklist status in one lookup.<\/li>\n\n\n\n<li><strong>Score a real send before launch.<\/strong> <a href=\"https:\/\/www.mail-tester.com\/\" target=\"_blank\" rel=\"noopener\">Mail Tester<\/a> flags authentication problems before users encounter them.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Related Issues You May Hit Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-authentication-error\/\">SMTP authentication errors<\/a> when credentials are rejected<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-connection-timeout\/\">SMTP connection timeouts<\/a> when the connection hangs<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-not-working\/\">SMTP not working<\/a> across the ten most common failure modes<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/why-emails-go-to-spam-in-gmail\/\">Emails landing in Gmail spam<\/a> despite a successful send<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-response-codes-explained\/\">SMTP response codes<\/a> for interpreting server replies<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently Asked Questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Why is there no JavaMailSender bean?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Spring Boot only creates it when <code>spring.mail.host<\/code> is set. Check the property exists in the profile that is active in the failing environment.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What are the default SMTP timeouts in Spring Boot?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Jakarta Mail waits indefinitely by default. Always set <code>connectiontimeout<\/code>, <code>timeout<\/code> and <code>writetimeout<\/code> explicitly.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why is my @Async email method not asynchronous?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Either <code>@EnableAsync<\/code> is missing, the method is not public, or it is called from within the same class, which bypasses the Spring proxy.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Should I use javax.mail or jakarta.mail?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Spring Boot 3 and later use <code>jakarta.mail<\/code>. Spring Boot 2 uses <code>javax.mail<\/code>. Mixing them causes compilation or runtime errors.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I send email with attachments?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Create a <code>MimeMessageHelper<\/code> with multipart set to true and call <code>addAttachment()<\/code> with a file name and resource.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I use Gmail SMTP with Spring Boot?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For testing, yes. For production, no. Google enforces low sending caps, throttles automated traffic, and can lock the account.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why does my service report unhealthy when email is down?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Actuator&#8217;s mail health indicator checks the SMTP connection. Disable it with <code>management.health.mail.enabled=false<\/code> if a mail outage should not mark the whole service as down.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Spring Boot&#8217;s mail support is well designed, but its defaults are tuned for convenience rather than production. The infinite timeout, the host-dependent bean, and the proxy rules around <code>@Async<\/code> are each invisible until they cause an incident.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Set explicit timeouts, enable STARTTLS deliberately, keep credentials in the environment, render templates with Thymeleaf, and run sending on a dedicated executor in a separate bean. That covers nearly every production failure the framework is capable of producing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What remains is infrastructure: whether your host permits outbound SMTP, and whether receiving providers trust your domain. A dedicated <a href=\"https:\/\/www.photonconsole.com\/relay.php\">transactional email solution<\/a> handles authentication, routing and reputation using the configuration shown above. Developers working across stacks may also want our guides to <a href=\"https:\/\/photonconsole.com\/blog\/how-to-send-emails-in-node-js-with-nodemailer-a-production-setup-guide\/\">sending email in Node.js<\/a> and <a href=\"https:\/\/photonconsole.com\/blog\/sending-email-in-python-smtplib-vs-an-email-api-with-working-code\/\">sending email in Python<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Read More<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/www.photonconsole.com\/email-deliverability-checker.php\">Free Email Deliverability Checker<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/transactional-email-queue-architecture-explained\/\">Transactional Email Queue Architecture Explained<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-retry-logic-explained-for-transactional-email-systems\/\">SMTP Retry Logic for Transactional Email Systems<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/spf-dkim-dmarc-explained-simply\/\">SPF, DKIM and DMARC Explained Simply<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-configuration\/\">SMTP Configuration: Complete Setup Reference<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.photonconsole.com\/relay.php\">PhotonRelay: SMTP Relay Service<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.photonconsole.com\/pricing.php\">PhotonConsole Pricing<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Spring Boot makes email look almost free. Add one starter dependency, set a few properties, inject JavaMailSender, and a test message goes out on the first try. Production is where the gaps show. A request thread hangs for minutes because a mail server stopped responding. The application refuses to start because a bean could not [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":433,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[312],"tags":[540,538,539],"class_list":["post-432","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-email-engineering-guide","tag-and-configure-javamailsender-properly-for-production","tag-fix-async","tag-spring-boot-email-hanging-or-not-sending-set-the-timeouts-most-tutorials-skip"],"_links":{"self":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/432","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/comments?post=432"}],"version-history":[{"count":1,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/432\/revisions"}],"predecessor-version":[{"id":436,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/432\/revisions\/436"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/media\/433"}],"wp:attachment":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/media?parent=432"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/categories?post=432"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/tags?post=432"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}