Remigiusz ZalewskiRemigiusz Zalewski

How to send an email in .NET

how-to-send-email-in-dotnet

Introduction

Password resets, order confirmations, notifications - sending email is a requirement on most applications, and the first version is usually simpler than people expect. This video sends email from an ASP.NET Core minimal API on .NET 8 using the built-in SmtpClient against Gmail's SMTP server, wrapped behind an interface and configured with the Options pattern.

It also covers the parts that trip people up: why your Gmail password will not work, where the credentials should actually live, and the point at which SmtpClient stops being the right answer.

🎬 Watch the full video here:


The service behind an interface

Email sending goes behind IMailService so the rest of the app depends on an abstraction, not on System.Net.Mail:

public interface IMailService
{
    Task SendEmailAsync(SendEmailRequest sendEmailRequest);
}

SendEmailRequest is a small record: recipient, subject, body. The endpoint is one line that takes the request and calls the service. When you later swap SMTP for a transactional email provider, only the implementation changes.

Configuration through the Options pattern

Host, port, sender address, and password are bound to a typed options class rather than read as loose configuration keys:

public class GmailOptions
{
    public const string GmailOptionsKey = "GmailOptions";
    public string Host { get; set; }
    public int Port { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
}
builder.Services.Configure<GmailOptions>(
    builder.Configuration.GetSection(GmailOptions.GmailOptionsKey));

The service takes IOptions<GmailOptions> and reads .Value. Gmail's SMTP is smtp.gmail.com on port 587 with SSL enabled.

Sending the message

The implementation builds a MailMessage, configures an SmtpClient with the host, port, credentials, and EnableSsl = true, and calls SendMailAsync. The SmtpClient is wrapped in using so the connection is disposed after the send.

using var smtpClient = new SmtpClient
{
    Host = _options.Host,
    Port = _options.Port,
    Credentials = new NetworkCredential(_options.Email, _options.Password),
    EnableSsl = true
};
await smtpClient.SendMailAsync(mailMessage);

The Gmail app password

Your normal Google account password will be rejected. Gmail requires an app password for SMTP: turn on 2-Step Verification, then generate a 16-character app password in your Google account security settings and use that as Password. This is the single most common reason "it does not work" for this exact setup.

Where credentials belong

Not in appsettings.json committed to source control. Locally use .NET user secrets (dotnet user-secrets set "GmailOptions:Password" "..."). In the cloud use environment variables or a secret store (Key Vault, AWS Secrets Manager). The Options binding does not change - only where the value comes from.

When to stop using SmtpClient

SmtpClient and a Gmail account are fine for a side project or internal tool with low volume. For anything customer-facing, move to a transactional email provider (SendGrid, Postmark, Amazon SES, Mailgun, Resend). Reasons:

  • Deliverability. Providers manage SPF, DKIM, DMARC, and sender reputation. Mail sent straight from an app server frequently lands in spam or is blocked outright.
  • Rate limits. Gmail caps sending sharply. A provider scales.
  • Observability. Providers give you delivery, bounce, and open tracking. SmtpClient gives you an exception or silence.
  • Microsoft's own guidance. The docs explicitly note SmtpClient is not recommended for new development and suggest a maintained library or a service.

Most providers offer an HTTP API and a NuGet package, so switching is a new IMailService implementation, not a rewrite.

Don't block the request on sending

Sending mail inline makes the HTTP response wait for an external SMTP handshake. For a password reset that is borderline acceptable; for a bulk notification it is not. Push the send onto a background job (Hangfire, a hosted service, a queue) so the request returns immediately and a failed send can be retried.

Common pitfalls

  • Using the account password instead of an app password. It will not authenticate.
  • Committing credentials. User secrets locally, environment/secret store in production.
  • Sending synchronously in the request path. Offload to a background worker.
  • Assuming it landed. Without a provider you have no delivery signal - a successful SendMailAsync only means the SMTP server accepted the handoff.
  • Not disposing SmtpClient. Wrap it in using.

Key Takeaways

  • Put email sending behind IMailService so the transport is swappable.
  • Bind SMTP settings to typed options; keep the password in a secret store, never in appsettings.json.
  • Gmail SMTP needs a generated app password plus 2-Step Verification, smtp.gmail.com:587, SSL on.
  • SmtpClient + Gmail is fine for low-volume internal use; move to a transactional provider for anything customer-facing (deliverability, limits, tracking).
  • Send from a background job, not inline in the request.

Get the Full Source Code

The complete runnable solution - the mail service, the options binding, and the minimal API endpoint - is available to Patreon supporters. If you want to send a real test email instead of rebuilding it from the walkthrough above, you can find it on Patreon.

Resources