Describe the message with OutgoingMessage and pass it to Mail.SendAsync with the server and your login. It connects, upgrades the connection with STARTTLS, logs in with the strongest mechanism the server offers, sends and says goodbye, in one call. The program below sends the same message as Create an EML file: two bodies, a CSV attachment and a sender name with accents.
dotnet new console -o SendEmail && cd SendEmail dotnet add package RecursiveAI.OneMail.Client --version 1.0.1
RecursiveAI.OneMail.Client brings in RecursiveAI.OneMail. Put this in Program.cs:
using System.Text; using RecursiveAI.OneMail; using RecursiveAI.OneMail.Client.Sasl; using RecursiveAI.OneMail.Client.Smtp; using RecursiveAI.OneMail.Composition; // The server and the login come from the environment string host = Environment.GetEnvironmentVariable("SMTP_HOST") ?? "smtp.example.com"; int port = int.Parse(Environment.GetEnvironmentVariable("SMTP_PORT") ?? "587"); var login = new SaslCredentials { UserName = Environment.GetEnvironmentVariable("SMTP_USER"), Password = Environment.GetEnvironmentVariable("SMTP_PASSWORD"), }; // Non-ASCII names and subjects are encoded for you; Date and Message-ID are added var message = new OutgoingMessage { From = "Zoë Martín <[email protected]>", To = ["Sam Lee <[email protected]>"], Subject = "Q3 report – final", Text = "Hi Sam,\r\n\r\nThe report is attached.\r\n", Html = "<p>Hi Sam,</p><p>The report is attached.</p>", Attachments = [new OutgoingAttachment("q3.csv", Csv(), "text/csv")], }; // Connect, STARTTLS (required by default), log in, send, QUIT SmtpSendResult result = await Mail.SendAsync(message, new SmtpClientOptions { Host = host, Port = port, Credentials = login }); // A refused recipient is data in the result, not an exception foreach (SmtpRecipientResult r in result.Recipients) { string verdict = r.Accepted ? "accepted" : "refused"; Console.WriteLine($"{r.Address}: {verdict} ({r.Reply.Code})"); } Console.WriteLine(result.Delivered ? $"Queued: {result.DataReply!.Code} {result.DataReply.Text}" : "Not accepted"); Console.WriteLine($"Sent {result.Size:N0} bytes, chunking: {result.UsedChunking}"); static byte[] Csv() => Encoding.UTF8.GetBytes("month,revenue\r\nJul,1200\r\nAug,1350\r\n");
Set SMTP_HOST, SMTP_PORT, SMTP_USER and SMTP_PASSWORD, then dotnet run. Against a local Mailpit test server with STARTTLS required, it printed:
[email protected]: accepted (250) Queued: 250 2.0.0 Ok: queued as 1smy8cZobLtp06wdVBlgVo Sent 916 bytes, chunking: False
The server received a multipart/mixed message with a text and an HTML part and the attachment. The sender’s name went out as =?utf-8?B?Wm/DqyBNYXJ0w61u?= and the dash in the subject as an encoded word, so any mail program shows them as written. Your server’s queue id will differ.
Security by default
SmtpClientOptions.Security starts at StartTls. In the docs’ words: “STARTTLS (RFC 3207) is required. If the server does not announce it, SmtpFeatureRequiredException("STARTTLS") is thrown and no credentials are sent.” For port 465, set Security = SmtpSecurity.ImplicitTls. The server’s certificate is checked as any .NET TLS connection checks it, so a local test server needs a certificate your machine trusts.
Gmail and Outlook.com want OAuth 2.0. The library “never acquires tokens”: get one yourself and pass it as SaslCredentials.AccessToken instead of Password. Endpoints and app setup: Gmail and Outlook.com.
The result
SmtpSendResult has the server’s reply for the sender, for each recipient and for the message. “A rejected sender or recipient is data in the result, never an exception.” Delivered is true when the server took the message for at least one recipient, and Complete when it took all of them. Call result.ThrowIfNotDelivered() if you prefer an exception. A Bcc address gets the message but isn’t in the headers that go out.
Limits
- Providers. From the project’s known gaps: “Outlook.com provider rows have not been run.” “Yahoo and iCloud have not been run either.” Gmail passed over SMTP, IMAP and POP3.
- x64 only. “The protocol rows are x64 only. The container protocol suites have not been run on arm64.”
- No queue. The client “does not queue or retry: a
4xxis reported in the result and the caller decides”, and it “does not look up MX records”: it sends to the server you name. - No signing. The client “does not sign”; DKIM and S/MIME aren’t in the 1.0.1 packages.
- Native platforms only. The client isn’t in the browser (WebAssembly) build.
Several messages over one connection, and every option: SMTP. Reading mail: IMAP and POP3. Writing the message to a file instead: Create an EML file.