{"id":425,"date":"2026-09-21T12:55:00","date_gmt":"2026-09-21T18:25:00","guid":{"rendered":"https:\/\/photonconsole.com\/blog\/?p=425"},"modified":"2026-09-21T07:24:46","modified_gmt":"2026-09-21T12:54:46","slug":"sending-email-in-net-and-c-smtpclient-mailkit-and-production-setup","status":"publish","type":"post","link":"https:\/\/photonconsole.com\/blog\/sending-email-in-net-and-c-smtpclient-mailkit-and-production-setup\/","title":{"rendered":"Sending Email in .NET and C#: SmtpClient, MailKit and Production Setup"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Most C# email tutorials start with <code>System.Net.Mail.SmtpClient<\/code>. It is built into the framework, the code is short, and it works on the first test. Then you point it at a production mail server on port 465 and the connection hangs until it times out. Or it works for a week and starts throwing under load.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft&#8217;s own documentation now recommends against using that class for new development. This guide explains why, shows the modern replacement, and covers the production pieces most examples skip: configuration, async sending, background queuing in ASP.NET Core, and the port setting that catches almost everyone.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Quick Answer: How Do You Send Email in C#?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use MailKit with an authenticated SMTP relay:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dotnet add package MailKit<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>using MailKit.Net.Smtp;\nusing MailKit.Security;\nusing MimeKit;\n\nvar message = new MimeMessage();\nmessage.From.Add(new MailboxAddress(\"Your App\", \"noreply@yourdomain.com\"));\nmessage.To.Add(MailboxAddress.Parse(\"user@example.com\"));\nmessage.Subject = \"Your verification code\";\nmessage.Body = new TextPart(\"plain\") { Text = \"Your code is 492019.\" };\n\nusing var client = new SmtpClient();\nawait client.ConnectAsync(\"smtp.photonrelay.com\", 587, SecureSocketOptions.StartTls);\nawait client.AuthenticateAsync(smtpUser, smtpPass);\nawait client.SendAsync(message);\nawait client.DisconnectAsync(true);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The library handles the protocol correctly. What decides delivery is the server you authenticate against and whether your domain is authorised to send. 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\">Why Not System.Net.Mail.SmtpClient?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/learn.microsoft.com\/en-us\/dotnet\/api\/system.net.mail.smtpclient\" target=\"_blank\" rel=\"noopener\">Microsoft documentation for SmtpClient<\/a> states that it is not recommended for new development because it does not support many modern protocols, and points developers to MailKit instead. The class still exists and still compiles, which is why so many tutorials continue to use it.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Capability<\/th><th>System.Net.Mail.SmtpClient<\/th><th>MailKit<\/th><\/tr><\/thead><tbody><tr><td>STARTTLS on port 587<\/td><td>Yes<\/td><td>Yes<\/td><\/tr><tr><td>Implicit SSL on port 465<\/td><td>No<\/td><td>Yes<\/td><\/tr><tr><td>True async API<\/td><td>Limited<\/td><td>Yes<\/td><\/tr><tr><td>Modern authentication methods<\/td><td>Limited<\/td><td>Yes<\/td><\/tr><tr><td>Detailed protocol errors<\/td><td>Minimal<\/td><td>Yes<\/td><\/tr><tr><td>Recommended for new code<\/td><td>No<\/td><td>Yes<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The second row causes the most production failures, covered next.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why .NET 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=\"577\" src=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-21-2026-11_52_54-AM-1024x577.png\" alt=\"Comparison of System.Net.Mail SmtpClient and MailKit showing that only MailKit supports implicit SSL on port 465\" class=\"wp-image-428\" style=\"aspect-ratio:1.7777777777777777;width:777px;height:auto\" srcset=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-21-2026-11_52_54-AM-1024x577.png 1024w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-21-2026-11_52_54-AM-300x169.png 300w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-21-2026-11_52_54-AM-767x432.png 767w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-21-2026-11_52_54-AM-1536x865.png 1536w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-21-2026-11_52_54-AM.png 1671w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">The built-in SmtpClient cannot do implicit SSL, which is why port 465 hangs.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Most failures come from configuration or authentication rather than faults in the framework. These five account for the majority.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Port 465 With System.Net.Mail<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The built-in <code>SmtpClient<\/code> only supports STARTTLS, where the connection starts plain and upgrades. Port 465 expects encryption from the first byte. Setting <code>EnableSsl = true<\/code> with port 465 does not produce implicit SSL \u2014 it produces a connection that waits for a plaintext greeting that never arrives, then times out.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. Credentials Committed in appsettings.json<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">SMTP passwords placed in <code>appsettings.json<\/code> end up in source control and in every build artefact. Use user secrets in development and environment variables or a secret store in production.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. Synchronous Sends Inside Request Handlers<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A blocking send inside a controller ties up a thread for the entire SMTP conversation. Under load this exhausts the thread pool and slows every request on the server, not only the ones sending mail.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. 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 authorised 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<h3 class=\"wp-block-heading\">5. Outbound SMTP Blocked by the Host<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">AWS, Google Cloud and Azure restrict outbound port 25 by default. AWS documents its <a href=\"https:\/\/repost.aws\/knowledge-center\/ec2-port-25-throttle\" target=\"_blank\" rel=\"noopener\">port 25 throttle removal process<\/a>, though removal does not solve the reputation problem underneath. Our breakdown of <a href=\"https:\/\/photonconsole.com\/blog\/smtp-connection-timeout\/\">SMTP connection timeouts<\/a> covers the diagnosis.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Common Mistake<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Using <code>System.Net.Mail.SmtpClient<\/code> with port 465 and <code>EnableSsl = true<\/code>, then spending hours checking credentials. The credentials are fine. That class cannot do implicit SSL, so the connection hangs before authentication is ever attempted. Switch to port 587, or move to MailKit with <code>SecureSocketOptions.SslOnConnect<\/code> if the provider requires 465.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Quick Fix<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SMTP Send Hangs or Times Out<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Use port 587 with STARTTLS, or MailKit with <code>SslOnConnect<\/code> for port 465<\/li>\n\n\n\n<li>Set an explicit timeout rather than relying on the default<\/li>\n\n\n\n<li>Try port 2525 if your host blocks both 587 and 465 outbound<\/li>\n\n\n\n<li>Test connectivity from the server itself, not from your development machine<\/li>\n\n\n\n<li>Check the exception&#8217;s inner exception \u2014 the useful detail is usually there<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Step-by-Step: Production Setup in ASP.NET Core<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Install MailKit<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>dotnet add package MailKit<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">MailKit brings MimeKit with it. Both are maintained in the <a href=\"https:\/\/github.com\/jstedfast\/MailKit\" target=\"_blank\" rel=\"noopener\">MailKit repository<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Note<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">MailKit&#8217;s <code>SmtpClient<\/code> has the same class name as <code>System.Net.Mail.SmtpClient<\/code>. If a file imports both namespaces you will get an ambiguous reference error. Remove the <code>System.Net.Mail<\/code> using directive, or fully qualify the type as <code>MailKit.Net.Smtp.SmtpClient<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Define Strongly Typed Settings<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>public class SmtpSettings\n{\n    public string Host { get; set; } = \"\";\n    public int Port { get; set; } = 587;\n    public string Username { get; set; } = \"\";\n    public string Password { get; set; } = \"\";\n    public string FromAddress { get; set; } = \"\";\n    public string FromName { get; set; } = \"\";\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ appsettings.json \u2014 no password here\n{\n  \"Smtp\": {\n    \"Host\": \"smtp.photonrelay.com\",\n    \"Port\": 587,\n    \"FromAddress\": \"noreply@yourdomain.com\",\n    \"FromName\": \"Your App\"\n  }\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># Development: store secrets outside the repository\ndotnet user-secrets set \"Smtp:Username\" \"your_project_api_user\"\ndotnet user-secrets set \"Smtp:Password\" \"your_secret_api_key\"\n\n# Production: environment variables use double underscores\nSmtp__Username=your_project_api_user\nSmtp__Password=your_secret_api_key<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Build an Email Service<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>using MailKit.Net.Smtp;\nusing MailKit.Security;\nusing Microsoft.Extensions.Options;\nusing MimeKit;\n\npublic interface IEmailSender\n{\n    Task SendAsync(string to, string subject, string html, CancellationToken ct = default);\n}\n\npublic class SmtpEmailSender : IEmailSender\n{\n    private readonly SmtpSettings _settings;\n    private readonly ILogger&lt;SmtpEmailSender&gt; _logger;\n\n    public SmtpEmailSender(IOptions&lt;SmtpSettings&gt; options, ILogger&lt;SmtpEmailSender&gt; logger)\n    {\n        _settings = options.Value;\n        _logger = logger;\n    }\n\n    public async Task SendAsync(string to, string subject, string html, CancellationToken ct = default)\n    {\n        var message = new MimeMessage();\n        message.From.Add(new MailboxAddress(_settings.FromName, _settings.FromAddress));\n        message.To.Add(MailboxAddress.Parse(to));\n        message.Subject = subject;\n\n        var builder = new BodyBuilder\n        {\n            HtmlBody = html,\n            TextBody = System.Text.RegularExpressions.Regex.Replace(html, \"&lt;.*?&gt;\", \"\")\n        };\n        message.Body = builder.ToMessageBody();\n\n        using var client = new SmtpClient { Timeout = 10000 };\n\n        var socketOptions = _settings.Port == 465\n            ? SecureSocketOptions.SslOnConnect\n            : SecureSocketOptions.StartTls;\n\n        try\n        {\n            await client.ConnectAsync(_settings.Host, _settings.Port, socketOptions, ct);\n            await client.AuthenticateAsync(_settings.Username, _settings.Password, ct);\n            await client.SendAsync(message, ct);\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Email to {Recipient} failed\", to);\n            throw;\n        }\n        finally\n        {\n            await client.DisconnectAsync(true, ct);\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note the plain text alternative in <code>TextBody<\/code>. HTML-only messages score worse with spam filters.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4: Register the Service<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Program.cs\nbuilder.Services.Configure&lt;SmtpSettings&gt;(builder.Configuration.GetSection(\"Smtp\"));\nbuilder.Services.AddTransient&lt;IEmailSender, SmtpEmailSender&gt;();<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Queueing Email with a Background Service<\/h2>\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-21-2026-11_56_00-AM-1024x577.png\" alt=\"ASP.NET Core email flow showing a controller writing to a channel queue that a background service delivers through an SMTP relay\" class=\"wp-image-429\" style=\"aspect-ratio:1.7777777777777777;width:1200px;height:auto\" srcset=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-21-2026-11_56_00-AM-1024x577.png 1024w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-21-2026-11_56_00-AM-300x169.png 300w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-21-2026-11_56_00-AM-767x432.png 767w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-21-2026-11_56_00-AM-1536x865.png 1536w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-21-2026-11_56_00-AM.png 1671w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">The controller returns immediately while a hosted background service handles delivery.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Awaiting a send inside a controller still makes the user wait for the SMTP handshake. For anything user-facing, push the message into an in-memory queue and let a <a href=\"https:\/\/learn.microsoft.com\/en-us\/aspnet\/core\/fundamentals\/host\/hosted-services\" target=\"_blank\" rel=\"noopener\">hosted background service<\/a> deliver it.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>using System.Threading.Channels;\n\npublic record EmailJob(string To, string Subject, string Html);\n\npublic class EmailQueue\n{\n    private readonly Channel&lt;EmailJob&gt; _channel =\n        Channel.CreateBounded&lt;EmailJob&gt;(new BoundedChannelOptions(1000)\n        {\n            FullMode = BoundedChannelFullMode.Wait\n        });\n\n    public ValueTask EnqueueAsync(EmailJob job) =&gt; _channel.Writer.WriteAsync(job);\n    public IAsyncEnumerable&lt;EmailJob&gt; ReadAllAsync(CancellationToken ct) =&gt;\n        _channel.Reader.ReadAllAsync(ct);\n}\n\npublic class EmailBackgroundService : BackgroundService\n{\n    private readonly EmailQueue _queue;\n    private readonly IServiceScopeFactory _scopes;\n    private readonly ILogger&lt;EmailBackgroundService&gt; _logger;\n\n    public EmailBackgroundService(EmailQueue queue, IServiceScopeFactory scopes,\n        ILogger&lt;EmailBackgroundService&gt; logger)\n    {\n        _queue = queue;\n        _scopes = scopes;\n        _logger = logger;\n    }\n\n    protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n    {\n        await foreach (var job in _queue.ReadAllAsync(stoppingToken))\n        {\n            for (var attempt = 1; attempt &lt;= 3; attempt++)\n            {\n                try\n                {\n                    using var scope = _scopes.CreateScope();\n                    var sender = scope.ServiceProvider.GetRequiredService&lt;IEmailSender&gt;();\n                    await sender.SendAsync(job.To, job.Subject, job.Html, stoppingToken);\n                    break;\n                }\n                catch (Exception ex) when (attempt &lt; 3)\n                {\n                    _logger.LogWarning(ex, \"Retry {Attempt} for {To}\", attempt, job.To);\n                    await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), stoppingToken);\n                }\n                catch (Exception ex)\n                {\n                    _logger.LogError(ex, \"Giving up on email to {To}\", job.To);\n                }\n            }\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Program.cs\nbuilder.Services.AddSingleton&lt;EmailQueue&gt;();\nbuilder.Services.AddHostedService&lt;EmailBackgroundService&gt;();\n\n\/\/ In a controller\nawait _emailQueue.EnqueueAsync(new EmailJob(user.Email, \"Welcome\", html));\nreturn Ok();<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">An in-memory channel is lost if the process restarts. For mail that must survive a restart, such as receipts or password resets, use a durable queue. Our guide to <a href=\"https:\/\/photonconsole.com\/blog\/transactional-email-queue-architecture-explained\/\">transactional email queue architecture<\/a> covers the trade-offs, 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<p class=\"wp-block-paragraph\">Quick Fix<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">MailKit Authentication Errors<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>AuthenticationException<\/code> \u2014 credentials wrong, or whitespace copied into them<\/li>\n\n\n\n<li><code>SslHandshakeException<\/code> \u2014 wrong socket option for the port<\/li>\n\n\n\n<li>Confirm environment variables use double underscores: <code>Smtp__Password<\/code><\/li>\n\n\n\n<li>Check user secrets are not being read in production, where they do not apply<\/li>\n\n\n\n<li>Log the full exception including inner exceptions before changing any setting<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Port Reference<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Port<\/th><th>MailKit option<\/th><th>When to use<\/th><\/tr><\/thead><tbody><tr><td>587<\/td><td><code>SecureSocketOptions.StartTls<\/code><\/td><td>Recommended default<\/td><\/tr><tr><td>465<\/td><td><code>SecureSocketOptions.SslOnConnect<\/code><\/td><td>When the provider requires implicit SSL<\/td><\/tr><tr><td>2525<\/td><td><code>SecureSocketOptions.StartTls<\/code><\/td><td>When the host blocks 587 and 465<\/td><\/tr><tr><td>25<\/td><td>n\/a<\/td><td>Avoid \u2014 blocked on nearly all hosts<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Configuring .NET With PhotonConsole<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Two steps: authenticate your domain in DNS, then supply the settings.<\/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>Smtp__Host=smtp.photonrelay.com\nSmtp__Port=587          # 465 with SslOnConnect, 2525 if 587 is blocked\nSmtp__Username=your_project_api_user\nSmtp__Password=your_secret_api_key\nSmtp__FromAddress=noreply@yourdomain.com\nSmtp__FromName=Your App<\/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 service, the background queue and retry handling 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\">Azure App Service<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Set settings under Configuration using double-underscore names. Azure restricts outbound port 25, so use 587 or 2525.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Docker and Kubernetes<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Containers have no local mail agent, so an external relay is required. Pass credentials as secrets, and remember that an in-memory email queue is lost whenever the pod restarts.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Azure Functions and AWS Lambda<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A hosted background service does not fit a function that ends after each invocation. Send directly with a short timeout, or hand the message to a durable queue that another function processes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Legacy .NET Framework<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">MailKit supports .NET Framework as well as modern .NET, so older applications can migrate without an upgrade.<\/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>Always call DisconnectAsync(true).<\/strong> It sends the SMTP QUIT command cleanly rather than dropping the socket.<\/li>\n\n\n\n<li><strong>Reuse one connection for batches.<\/strong> Connect and authenticate once, send several messages, then disconnect.<\/li>\n\n\n\n<li><strong>Send from your own domain.<\/strong> Put a visitor&#8217;s address in Reply-To, never From, or SPF and DMARC fail.<\/li>\n\n\n\n<li><strong>Never log the settings object.<\/strong> It contains 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 early.<\/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-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\/emails-sent-but-not-delivered\/\">Emails sent but not delivered<\/a> when the server reports success<\/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\">Is System.Net.Mail.SmtpClient deprecated?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft&#8217;s documentation recommends against it for new development and points to MailKit instead. It still compiles and runs, but lacks implicit SSL and modern protocol support.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why does SmtpClient hang on port 465?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The built-in class only supports STARTTLS. Port 465 expects encryption from the first byte, so the connection waits for a greeting that never arrives. Use port 587, or MailKit with <code>SslOnConnect<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Should email be sent synchronously from a controller?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. Queue it and return immediately. A blocking send holds a thread for the entire SMTP conversation and degrades the whole application under load.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Where should SMTP credentials live?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">User secrets in development, environment variables or a secret manager in production. Never in <code>appsettings.json<\/code> committed to the repository.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I use Gmail SMTP from a .NET application?<\/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\">Does MailKit work with .NET Framework?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. It supports both .NET Framework and modern .NET, which makes it a straightforward migration path for older applications.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The built-in <code>SmtpClient<\/code> still appears in most C# tutorials, and it still works for simple cases on port 587. It fails on port 465, offers limited async support, and is no longer recommended by Microsoft for new code.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">MailKit replaces it with a small amount of change, and the production pattern around it is consistent: strongly typed settings, credentials outside source control, an injected sender service, and a background queue so users never wait on the SMTP handshake.<\/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>Most C# email tutorials start with System.Net.Mail.SmtpClient. It is built into the framework, the code is short, and it works on the first test. Then you point it at a production mail server on port 465 and the connection hangs until it times out. Or it works for a week and starts throwing under load. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":427,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3,1],"tags":[5,537,536],"class_list":["post-425","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-email-deliverability","category-uncategorized","tag-email-deliverability","tag-plus-a-production-setup-with-background-queuing-in-asp-net-core","tag-smtpclient-hanging-on-port-465-in-c-here-is-why-microsoft-recommends-mailkit-instead"],"_links":{"self":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/425","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=425"}],"version-history":[{"count":1,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/425\/revisions"}],"predecessor-version":[{"id":430,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/425\/revisions\/430"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/media\/427"}],"wp:attachment":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/media?parent=425"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/categories?post=425"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/tags?post=425"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}