Source: https://cli.nylas.com/guides/send-email-csharp

# Send Email in C#: MailKit, Graph API, CLI

Microsoft no longer recommends SmtpClient for new.NET code. Compare MailKit over SMTP, the Microsoft Graph SDK, and a CLI subprocess, with runnable C# code and an ASP.NET Core 2FA endpoint.

Written by [Hazik](https://cli.nylas.com/authors/hazik) Director of Product Management

Reviewed by [Qasim Muhammad](https://cli.nylas.com/authors/qasim-muhammad)

Updated June 9, 2026

> **TL;DR:** Microsoft's docs tell you not to use `SmtpClient` for new C# code. MailKit is the recommended SMTP replacement (about 5 minutes of setup), the Graph SDK covers Microsoft 365 mailboxes with OAuth (15+ minutes), and a `Process.Start` call to a CLI binary sends through 6 providers with zero NuGet packages. The last section turns that pattern into an ASP.NET Core endpoint that emails a 6-digit 2FA code.

> **Disclosure:** Nylas CLI is built by Nylas, Inc. This comparison reflects our testing and product understanding as of June 9, 2026.

Full documentation for the commands below: [`nylas email send`](https://cli.nylas.com/docs/commands/email-send) for the Process.Start pattern, [`nylas auth login`](https://cli.nylas.com/docs/commands/auth-login) for interactive OAuth, and [`nylas auth config`](https://cli.nylas.com/docs/commands/auth-config) for key-based headless setup.

## Why does Microsoft warn against SmtpClient in C#?

To send email in C# today, skip `System.Net.Mail.SmtpClient`. Microsoft's own [.NET API reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.mail.smtpclient) states: “We don't recommend that you use the SmtpClient class for new development because SmtpClient doesn't support many modern protocols. Use MailKit or other libraries instead.”

The class was designed around [RFC 2821-era SMTP](https://datatracker.ietf.org/doc/html/rfc2821) (the protocol spec was superseded by RFC 5321 in October 2008) and never gained first-class OAuth 2.0 support. That gap bites hardest on Microsoft 365: [Microsoft's SMTP AUTH documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission) steers client submission toward OAuth and notes that tenants with security defaults enabled have SMTP AUTH disabled outright. Gmail still accepts app passwords on accounts with 2-Step Verification, but plain account passwords are long gone there too. That leaves C# developers with three practical paths: MailKit when you control SMTP credentials, the Microsoft Graph SDK when the mailbox lives in Microsoft 365, and a CLI subprocess when you want one code path across providers.

## How do I send email in C# with MailKit?

MailKit is the open-source mail library that Microsoft's SmtpClient documentation points to by name. Install it with `dotnet add package MailKit` from [NuGet](https://www.nuget.org/packages/MailKit), build a MimeMessage, and send over SMTP. Gmail setup takes about 5 minutes: enable 2-Step Verification and generate a 16-character [app password](https://support.google.com/accounts/answer/185833). That credential has been mandatory for personal Gmail since May 2022, when Google ended Less Secure Apps for consumer accounts; Workspace held on until the final cutoff hit on May 1, 2025.

The code below connects to Gmail's SMTP server on port 587 with STARTTLS and authenticates with that app password. Daily ceilings apply: a personal address can send 500 messages, a Workspace mailbox 2,000. For Outlook, swap the host to `smtp.office365.com` — and plan on an OAuth token rather than a password, since Microsoft 365 tenants with security defaults enabled have SMTP AUTH turned off; MailKit handles OAuth via its SASL mechanisms.

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

var message = new MimeMessage();
message.From.Add(new MailboxAddress("Your Name", "you@gmail.com"));
message.To.Add(MailboxAddress.Parse("recipient@example.com"));
message.Subject = "Quarterly report";
message.Body = new TextPart("plain") { Text = "See the attached spreadsheet." };

using var client = new SmtpClient(); // MailKit.Net.Smtp, not System.Net.Mail
await client.ConnectAsync("smtp.gmail.com", 587, SecureSocketOptions.StartTls);
await client.AuthenticateAsync("you@gmail.com", "your-16-char-app-password");
await client.SendAsync(message);
await client.DisconnectAsync(true);
```

The tradeoff is credential management. App passwords are provider-specific, don't expire on their own, and grant full mailbox access — one leaked 16-character string exposes everything. The [MailKit repository](https://github.com/jstedfast/MailKit) documents OAuth 2.0 flows for Gmail and Office 365, but wiring token refresh into a background worker is code you have to own.

## How do I send email in C# with the Microsoft Graph SDK?

The Microsoft Graph SDK sends email from Microsoft 365 mailboxes through the [sendMail endpoint](https://learn.microsoft.com/en-us/graph/api/user-sendmail) over HTTPS, with no SMTP involved. You need an Entra ID app registration with the `Mail.Send` permission, which adds 15+ minutes of portal setup before the first message leaves your machine.

The example below uses the Graph SDK v5 pattern with `InteractiveBrowserCredential` for the OAuth consent flow, then posts to `me/sendMail`. Per Microsoft's [throttling documentation](https://learn.microsoft.com/en-us/graph/throttling-limits), the Outlook service allows 10,000 API requests per app per mailbox in a 10-minute window, with a maximum of 4 concurrent requests.

```csharp
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.Graph.Me.SendMail;
using Microsoft.Graph.Models;

var credential = new InteractiveBrowserCredential(
    new InteractiveBrowserCredentialOptions { ClientId = "<your-app-client-id>" });
var graph = new GraphServiceClient(credential, new[] { "Mail.Send" });

await graph.Me.SendMail.PostAsync(new SendMailPostRequestBody
{
    Message = new Message
    {
        Subject = "Quarterly report",
        Body = new ItemBody { ContentType = BodyType.Text, Content = "See the attached spreadsheet." },
        ToRecipients = new List<Recipient>
        {
            new() { EmailAddress = new EmailAddress { Address = "recipient@example.com" } },
        },
    },
    SaveToSentItems = true,
});
```

Graph is Microsoft-only. A Gmail or Yahoo sender needs an entirely different SDK, and the access token expires after roughly an hour, so production code depends on the credential class refreshing it silently. For a deeper walkthrough of app registration and permissions, see the [Microsoft Graph email quickstart](https://cli.nylas.com/guides/microsoft-graph-email-quickstart).

## How do I send email from C# with a CLI subprocess?

A subprocess call hands SMTP, OAuth token refresh, and MIME encoding to an external binary, so your C# project ships with zero email packages. `System.Diagnostics.Process` runs `nylas email send` and captures stdout; the CLI authenticates against Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP from one command. Setup takes about 2 minutes: `brew install nylas/nylas-cli/nylas` (other methods in the [getting started guide](https://cli.nylas.com/guides/getting-started)), then authenticate once.

The `--json` flag returns structured output for `JsonDocument.Parse`, and `--yes` skips the interactive confirmation so the call works in services and CI. Using `ArgumentList` instead of a concatenated argument string means recipient addresses and subject lines are passed as discrete arguments — no shell, no injection surface.

```csharp
using System.Diagnostics;
using System.Text.Json;

static JsonDocument SendEmail(string to, string subject, string body)
{
    var psi = new ProcessStartInfo("nylas") { RedirectStandardOutput = true };
    foreach (var arg in new[] { "email", "send",
        "--to", to, "--subject", subject, "--body", body, "--json", "--yes" })
    {
        psi.ArgumentList.Add(arg);
    }

    using var process = Process.Start(psi)!;
    var stdout = process.StandardOutput.ReadToEnd();
    process.WaitForExit();
    if (process.ExitCode != 0)
        throw new InvalidOperationException($"nylas exited with code {process.ExitCode}");
    return JsonDocument.Parse(stdout);
}

using var result = SendEmail(
    "recipient@example.com", "Quarterly report", "See the attached spreadsheet.");
Console.WriteLine(result.RootElement.GetProperty("id").GetString());
```

Spawning a process adds roughly 100ms per send compared to a pooled SMTP connection, so MailKit wins for batch jobs over 1,000 messages. For transactional volume (signup confirmations, alerts, OTP codes) the overhead is invisible and switching a sender from Gmail to Outlook requires zero code changes. PowerShell teams replacing `Send-MailMessage` use the same binary; see [replace Send-MailMessage](https://cli.nylas.com/guides/replace-send-mailmessage).

## How do I send a 2FA code from ASP.NET Core?

An ASP.NET Core 2FA endpoint generates a one-time password, emails it through the subprocess pattern above, and stores the code with an expiry for later verification. `RandomNumberGenerator.GetInt32` draws from the OS cryptographic RNG — never `System.Random` for security codes. A 6-digit OTP gives 1,000,000 combinations, which is safe when paired with a 5-minute expiry and an attempt limit.

The minimal API below maps a POST endpoint that accepts an email address, generates the code, and shells out to the CLI. In testing, the full request (OTP generation plus the subprocess send) completes in well under 1 second. Rate-limit the route (ASP.NET Core 7+ ships `AddRateLimiter`) so an attacker can't spam codes to arbitrary addresses.

```csharp
using System.Diagnostics;
using System.Security.Cryptography;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache();
var app = builder.Build();

app.MapPost("/auth/send-otp", async (OtpRequest req, IMemoryCache cache) =>
{
    var code = RandomNumberGenerator.GetInt32(0, 1_000_000).ToString("D6");
    cache.Set($"otp:{req.Email}", code, TimeSpan.FromMinutes(5));

    var psi = new ProcessStartInfo("nylas");
    foreach (var arg in new[] { "email", "send",
        "--to", req.Email,
        "--subject", "Your verification code",
        "--body", $"Your code is {code}. It expires in 5 minutes.",
        "--json", "--yes" })
    {
        psi.ArgumentList.Add(arg);
    }

    using var process = Process.Start(psi)!;
    await process.WaitForExitAsync();
    return process.ExitCode == 0 ? Results.Ok() : Results.StatusCode(502);
});

app.Run();

record OtpRequest(string Email);
```

The same endpoint built on MailKit needs credential storage and rotation; built on Graph it needs an app registration and works only for Microsoft 365 senders. The subprocess version is 30 lines and provider-neutral. Node.js teams get the identical pattern with `child_process` in the [Node.js guide](https://cli.nylas.com/guides/send-email-nodejs).

## Next steps

- [Send email from Node.js](https://cli.nylas.com/guides/send-email-nodejs) — the same 3-method comparison for JavaScript backends
- [Microsoft Graph email quickstart](https://cli.nylas.com/guides/microsoft-graph-email-quickstart) — app registration, permissions, and sendMail in depth
- [Replace Send-MailMessage](https://cli.nylas.com/guides/replace-send-mailmessage) — the PowerShell equivalent of retiring SmtpClient
- [Twilio SendGrid vs Nylas](https://cli.nylas.com/guides/twilio-vs-nylas) — when a transactional email API fits better than SMTP
- [EmailEngine vs Nylas](https://cli.nylas.com/guides/emailengine-vs-nylas) — self-hosted vs hosted email infrastructure compared
- [Command reference](https://cli.nylas.com/docs/commands) — every email flag and subcommand documented
- [SmtpClient API reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.mail.smtpclient) — Microsoft's recommendation against SmtpClient, verbatim
- [Graph sendMail reference](https://learn.microsoft.com/en-us/graph/api/user-sendmail) — request shape, permissions, and limits
- [MailKit on GitHub](https://github.com/jstedfast/MailKit) — SMTP, OAuth 2.0, and IMAP documentation
