Email Sending Fails After SSL Certificate Renewal — MailKit / ASP.NET Core SMTP Fix Print

  • 0

Symptom

Your ASP.NET Core or .NET Framework application suddenly stops sending email through our SmarterMail server. The exception text typically includes one or more of:

  • MailKit.Security.SslHandshakeException
  • An error occurred while attempting to establish an SSL or TLS connection
  • A required certificate is not within its validity period when verifying against the current system clock or the timestamp in the signed file
  • The remote certificate is invalid according to the validation procedure

The error appears suddenly — your code did not change, your appsettings.json did not change, and the same SMTP host/port worked for weeks or months prior. Webmail (login via browser) continues to work normally.

Quick Fix (1 minute)

Recycle your IIS application pool. This clears the cached SMTP/TLS connection inside your .NET process. Email sending will resume immediately.

  1. Log into Plesk
  2. Open Websites & Domains → your domain → IIS Settings (or Dedicated IIS Application Pool)
  3. Click Recycle

If the recycle fixes it, the root cause is the connection-pool issue described below — and it will happen again on the next certificate rotation (every 60-90 days) unless your code is updated.

Why This Happens

Our SmarterMail server uses Let's Encrypt SSL certificates, which renew automatically every 60-90 days. The renewal swaps the live certificate on the server in milliseconds, but your .NET application is doing something completely normal that creates the problem:

MailKit Pools TLS Connections

The popular MailKit.Net.Smtp.SmtpClient library — and to a lesser extent System.Net.Mail.SmtpClient — keeps SMTP connections alive between send calls for performance. A connection established before the certificate rotation is bound to a TLS session that authenticated against the previous certificate. When you reuse that connection after the rotation:

  • The TCP socket is still open
  • The TLS session state still references the old certificate chain
  • If the old certificate has now passed its notAfter date (which it has — that's why it was renewed), MailKit fails the validity check on the cached chain
  • You see the error above

This is not a server-side problem. The certificate on our server is healthy at the time of your error — we can verify this any time on demand. The problem is purely the cached TLS session in your application's memory.

Verifying Our Server-Side Certificate

From any Windows machine with internet access, you can verify the current cert on the SMTPS port (465) directly:

$tcp = New-Object Net.Sockets.TcpClient('ec2amaz-a4g4262.adaptivewebhosting.com', 465)
$ssl = New-Object Net.Security.SslStream($tcp.GetStream(), $false, { $true })
$ssl.AuthenticateAsClient('ec2amaz-a4g4262.adaptivewebhosting.com')
$cert = [Security.Cryptography.X509Certificates.X509Certificate2]$ssl.RemoteCertificate
"Subject:    $($cert.Subject)"
"Issuer:     $($cert.Issuer)"
"NotBefore:  $($cert.NotBefore)"
"NotAfter:   $($cert.NotAfter)"
$tcp.Close()

Or from a Linux/macOS machine:

openssl s_client -connect ec2amaz-a4g4262.adaptivewebhosting.com:465 -servername ec2amaz-a4g4262.adaptivewebhosting.com < /dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates

If the NotAfter date is in the future, the server cert is fine and the issue is in your application's connection pool.

The Permanent Fix

Update your email-sending code to either (a) create a fresh SmtpClient per send, or (b) catch and recover from cert-validation exceptions. Option (a) is simpler and recommended for typical notification-email volumes.

Option A — Fresh SmtpClient Per Send (recommended)

Replace any long-lived SmtpClient instance with a per-send using block. The TCP handshake overhead is negligible for typical notification emails (transactional, password reset, invoice, etc.).

using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;

public async Task SendEmailAsync(MimeMessage message)
{
    using (var client = new SmtpClient())
    {
        await client.ConnectAsync(
            "ec2amaz-a4g4262.adaptivewebhosting.com",
            465,
            SecureSocketOptions.SslOnConnect);

        await client.AuthenticateAsync(_smtpUser, _smtpPass);
        await client.SendAsync(message);
        await client.DisconnectAsync(true);
    }
}

Option B — Catch and Reconnect (if you need to keep pooling)

If high throughput requires keeping a pooled SmtpClient, wrap each send and reconnect on cert failures:

private SmtpClient _client;
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);

public async Task SendEmailAsync(MimeMessage message)
{
    await _lock.WaitAsync();
    try
    {
        for (int attempt = 0; attempt < 2; attempt++)
        {
            try
            {
                if (_client == null || !_client.IsConnected)
                {
                    _client?.Dispose();
                    _client = new SmtpClient();
                    await _client.ConnectAsync(
                        "ec2amaz-a4g4262.adaptivewebhosting.com",
                        465,
                        SecureSocketOptions.SslOnConnect);
                    await _client.AuthenticateAsync(_smtpUser, _smtpPass);
                }

                await _client.SendAsync(message);
                return;
            }
            catch (Exception ex) when (
                ex is SslHandshakeException ||
                ex is System.IO.IOException ||
                ex is ServiceNotConnectedException ||
                ex is ServiceNotAuthenticatedException)
            {
                _client?.Dispose();
                _client = null;
                if (attempt == 1) throw;
            }
        }
    }
    finally
    {
        _lock.Release();
    }
}

System.Net.Mail.SmtpClient (Older Code)

If you're using the older System.Net.Mail.SmtpClient (which Microsoft has marked as legacy), the same pattern applies — create a fresh instance per send, wrapped in a using block. System.Net.Mail.SmtpClient has its own internal connection pool which also caches TLS sessions. The fix is identical.

SMTP Settings (Reference)

SettingValue
Hostec2amaz-a4g4262.adaptivewebhosting.com
Port (recommended)465 with implicit TLS (SslOnConnect / SSL=true)
Port (alternate)587 with STARTTLS
AuthenticationRequired
UsernameFull email address (e.g., [email protected])
PasswordMailbox password (set in SmarterMail)

Same Issue, Other Languages

This connection-pooling pattern is not unique to .NET / MailKit. The same issue affects long-lived SMTP clients in any language. Equivalent fixes:

  • Java (JavaMail / Jakarta Mail): Don't cache a Session+Transport pair across requests. Create a fresh Transport per send, or catch MessagingException with a cert-related cause and reconnect.
  • Python (smtplib): Don't hold an SMTP_SSL instance globally. Use with smtplib.SMTP_SSL(...) as smtp: per send.
  • Node.js (nodemailer): Set pool: false for low-volume transactional, or call transporter.close() + recreate on ESOCKET/ECONNECTION errors.

Why We Use Let's Encrypt

Let's Encrypt is the industry-standard free certificate authority used by the majority of TLS deployments worldwide. Certificates are valid for 90 days and auto-renew. This is not specific to Adaptive Web Hosting — the same renewal cadence applies to any SmarterMail, Microsoft 365, Google Workspace, or AWS SES endpoint your application connects to. Code written to be resilient to certificate rotation (Option A above) will work across all providers without modification.

Summary

  • Immediate: recycle your IIS application pool
  • Permanent: create a fresh SmtpClient per send (Option A) OR catch SslHandshakeException and reconnect (Option B)
  • Don't need to change: SMTP host, port, credentials, or any SmarterMail settings

If your application continues to fail after both the recycle AND a code change above, open a support ticket and we'll dig deeper. The PowerShell snippet under "Verifying Our Server-Side Certificate" gives us a quick way to confirm the cert is healthy at the moment your error reproduces.


Was this answer helpful?

« Back