How to Send Email with Attachment in C# (MailKit and System.Net.Mail)

To send email with attachment in C#, build the message with MailKit’s BodyBuilder, call builder.Attachments.Add() with a file path, a byte array or a stream, and send it with MailKit’s SmtpClient. Microsoft now advises against System.Net.Mail.SmtpClient for new code.

I sent every message below on .NET 10 to a local test SMTP server, so you’ll see what actually arrived: attachment names, MIME types and byte counts.

The Sample Attachments and Local SMTP Server We’ll Use

The running sample is Bayview Supply Co., a made-up restaurant supplier in San Diego. Jenna Morales in the orders team emails invoice 10482 to Marcus Reed at Harbor Point Cafe.

The demo folder holds these files:

  • Attachments/Invoice-10482.pdf (43,015 bytes)
  • Attachments/PackingSlip-10482.pdf (33,030 bytes)
  • Attachments/DeliveryPhoto-10482.jpg (99,526 bytes), the biggest file, which matters for size limits
  • Assets/bayview-logo.png (5,156 bytes), the logo we’ll embed inline instead of attaching

I didn’t want to send real email or use a real mailbox, so every program talks to smtp4dev, a free fake SMTP server. It accepts mail on localhost and keeps it, and nothing leaves your PC.

It installs from NuGet as a .NET tool. I put it in its own folder and started it on port 2525, with its web page and API on port 5025:

dotnet tool install Rnwood.Smtp4dev --version 3.15.0 --tool-path C:\DotNetDemo\_smtp4dev
C:\DotNetDemo\_smtp4dev\smtp4dev --smtpport=2525 --urls=http://localhost:5025 --db=

Every example is a .NET 10 file-based app, so dotnet run AttachFile.cs is all you need. The #:package line pulls MailKit 4.18.2 from NuGet; if file-based apps are new to you, see what’s new in .NET 10.

To prove what arrived, Inbox.cs downloads the newest message from smtp4dev and lists each MIME part. I run it after every send:

#:package MimeKit@4.18.2
#:property PublishAot=false
using System.Net.Http.Json;
using MimeKit;

using var http = new HttpClient { BaseAddress = new("http://localhost:5025/") };
var page = await http.GetFromJsonAsync<Page>("api/messages?pageSize=1");
string id = page!.Results[0].Id;
var msg = await MimeMessage.LoadAsync(await http.GetStreamAsync($"api/messages/{id}/download"));

Console.WriteLine($"{msg.Subject} -> {msg.To}");
Console.WriteLine($"Body: {msg.Body!.ContentType.MimeType}");
foreach (var part in msg.BodyParts.OfType<MimePart>())
{
    using var bytes = new MemoryStream();
    part.Content!.DecodeTo(bytes);
    string how = part.IsAttachment ? "attachment" : "inline";
    Console.WriteLine($"  {part.ContentType.MimeType,-16} {how,-10} {part.FileName ?? "-",-24} {bytes.Length,6} B");
}

record Page(List<Item> Results);
record Item(string Id);

It turns off Native AOT with #:property PublishAot=false, because the reflection-based JSON call needs it in a file-based app. The output shows the subject, the body type and one line per part.

Why MailKit Instead of SmtpClient for Email Attachments?

Microsoft Learn’s SmtpClient page says it plainly: the class isn’t recommended for new development because it doesn’t support many modern protocols. It points you to MailKit or other libraries instead.

System.Net.Mail.SmtpClient still ships in .NET 10 and still works, so you don’t have to rewrite old code tomorrow. For anything new, I use MailKit, which builds messages with MimeKit and supports OAuth 2.0 sign-in.

Method 1: Attach a File to an Email with BodyBuilder.Attachments.Add (Recommended)

Use this when the file already sits on disk: an invoice your app saved, a report in a shared folder, a file a user picked. Here’s invoice 10482 going out:

#:package MailKit@4.18.2
using MailKit.Net.Smtp;
using MimeKit;

var message = new MimeMessage { Subject = "Invoice 10482" };
message.From.Add(MailboxAddress.Parse("orders@bayviewsupply.example"));
message.To.Add(MailboxAddress.Parse("marcus@harborpointcafe.example"));

var builder = new BodyBuilder { TextBody = "Your October invoice is attached." };
builder.Attachments.Add(Path.Combine("Attachments", "Invoice-10482.pdf"));
message.Body = builder.ToMessageBody();

using var smtp = new SmtpClient();
await smtp.ConnectAsync("localhost", 2525);
Console.WriteLine($"Server reply: {await smtp.SendAsync(message)}");
await smtp.DisconnectAsync(true);

Output:

Server reply: Mail accepted

Invoice 10482 -> marcus@harborpointcafe.example
Body: multipart/mixed
  text/plain       inline     -                            33 B
  application/pdf  attachment Invoice-10482.pdf         43015 B
VS Code running AttachFile.cs: MailKit BodyBuilder.Attachments.Add sends Invoice-10482.pdf to the local smtp4dev server and Inbox.cs shows the application/pdf attachment
One Attachments.Add call: the PDF arrives as a 43,015-byte application/pdf attachment

The server answered “Mail accepted”, and the received message is multipart/mixed with two parts: the 33-byte text body and the full 43,015-byte PDF.

How does this code work?

MimeMessage holds the headers. BodyBuilder collects the text body and attachments, and ToMessageBody() turns them into the MIME tree, multipart/mixed as soon as there’s an attachment.

Attachments.Add(path) reads the file, takes its name for the attachment and picks the content type from the extension, so .pdf became application/pdf without any extra code.

ConnectAsync, SendAsync and DisconnectAsync(true) are the SMTP conversation. The true sends QUIT so the server closes the session cleanly.

MailboxAddress.Parse keeps the address lines short. Use new MailboxAddress("Jenna Morales", "orders@bayviewsupply.example") when you want a display name next to the address.

Note: ConnectAsync(host, port) uses SecureSocketOptions.Auto, which turns on STARTTLS when the server offers it; smtp4dev doesn’t. For a real provider, use port 587 with SecureSocketOptions.StartTls and call AuthenticateAsync before sending.

In a Windows Forms app, the file usually comes from a file picker. Pass OpenFileDialog.FileName to Attachments.Add. The FolderBrowserDialog guide shows the same dialog pattern for whole folders.

Method 2: Send an Attachment from a Byte Array or MemoryStream

Reports you generate in code don’t need a temp file. Same orders, now as a CSV built in memory and attached straight from a byte[], plus a small note from a MemoryStream:

#:package MailKit@4.18.2
using MailKit.Net.Smtp;
using MimeKit;

byte[] csv = "Order,Total\n10480,612.50\n10482,919.02\n"u8.ToArray();
var builder = new BodyBuilder { TextBody = "October orders attached." };
builder.Attachments.Add("orders-2026-10.csv", csv, ContentType.Parse("text/csv"));
using (var note = new MemoryStream("Balance due: $919.02"u8.ToArray()))
    builder.Attachments.Add("balance.txt", note);   // copied right here

var message = new MimeMessage { Subject = "October orders (CSV)" };
message.From.Add(MailboxAddress.Parse("orders@bayviewsupply.example"));
message.To.Add(MailboxAddress.Parse("marcus@harborpointcafe.example"));
message.Body = builder.ToMessageBody();

using var smtp = new SmtpClient();
await smtp.ConnectAsync("localhost", 2525);
Console.WriteLine($"{csv.Length}-byte CSV sent: {await smtp.SendAsync(message)}");
await smtp.DisconnectAsync(true);

Output:

38-byte CSV sent: Mail accepted

October orders (CSV) -> marcus@harborpointcafe.example
Body: multipart/mixed
  text/plain       inline     -                            24 B
  text/csv         attachment orders-2026-10.csv           41 B
  text/plain       attachment balance.txt                  20 B
VS Code running AttachMemory.cs: a C# email attachment from a byte array (orders-2026-10.csv, text/csv) and from a MemoryStream (balance.txt) received by smtp4dev
Both in-memory attachments arrive; the 38-byte CSV comes out as 41 bytes

Both attachments arrived: orders-2026-10.csv as text/csv and balance.txt as text/plain, 20 bytes.

How does this code work?

The u8 suffix turns the CSV literal into UTF-8 bytes. For text you build at run time, Encoding.UTF8.GetBytes does the same job; that’s how I convert a string to a byte array in C#.

The three-argument Add(fileName, bytes, contentType) sets the type you pass instead of guessing from the name.

The stream overload needs only a name, and the type came from the .txt extension. The using block disposed the stream before the message was even built, and balance.txt still arrived, because MimeKit copied the data when I called Add.

So here’s the catch: the CSV left as 38 bytes and arrived as 41. Text attachments travel with CRLF line endings, so each of my three \n endings gained a \r. Build the CSV with \r\n if a byte-exact match matters.

For bigger exports, a StringBuilder instead of string concatenation keeps the CSV fast, and string.Join turns a list into a comma-separated line.

Method 3: Send Multiple Attachments in One Email

Order 10482 has three files. Call Attachments.Add once per file; a foreach loop in C# over the folder does it for any number of files. This version also measures the message and sends with a timeout:

#:package MailKit@4.18.2
using MailKit.Net.Smtp;
using MimeKit;

var message = new MimeMessage { Subject = "Order 10482 documents" };
message.From.Add(MailboxAddress.Parse("orders@bayviewsupply.example"));
message.To.Add(MailboxAddress.Parse("marcus@harborpointcafe.example"));

var builder = new BodyBuilder { TextBody = "Everything for order 10482 is attached." };
string[] files = Directory.GetFiles("Attachments");
foreach (string file in files)
    builder.Attachments.Add(file);
message.Body = builder.ToMessageBody();

long onDisk = files.Sum(f => new FileInfo(f).Length);
using var mime = new MemoryStream();
message.WriteTo(mime);
Console.WriteLine($"{files.Length} files: {onDisk} bytes on disk, {mime.Length} bytes as MIME");

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var smtp = new SmtpClient();
await smtp.ConnectAsync("localhost", 2525, cancellationToken: cts.Token);
Console.WriteLine($"Server size limit (SIZE): {smtp.MaxSize}");
await smtp.SendAsync(message, cts.Token);
await smtp.DisconnectAsync(true, cts.Token);

Output:

3 files: 175571 bytes on disk, 241262 bytes as MIME
Server size limit (SIZE): 0

Order 10482 documents -> marcus@harborpointcafe.example
Body: multipart/mixed
  text/plain       inline     -                            39 B
  image/jpeg       attachment DeliveryPhoto-10482.jpg   99526 B
  application/pdf  attachment Invoice-10482.pdf         43015 B
  application/pdf  attachment PackingSlip-10482.pdf     33030 B

All three files arrived with the right types: image/jpeg for the photo and application/pdf for both PDFs.

How does this code work?

Directory.GetFiles returns every file in Attachments, and the loop adds each one. message.WriteTo writes the finished MIME message to a MemoryStream, which is exactly what goes over the wire.

That’s where the size surprise shows up. The files total 175,571 bytes, but the message is 241,262 bytes, about 37% bigger, because base64 encoding turns every 3 bytes into 4 characters plus line breaks.

Mail servers cap the encoded message, not your files, so check that number against your provider’s limit. MailKit’s MaxSize reads the server’s SIZE extension; smtp4dev doesn’t announce one, so it printed 0.

The CancellationTokenSource gives the whole send 30 seconds. Pass the token to every async call so a stalled server can’t hang a background job forever.

Pro Tip: past a few megabytes, upload the file to SharePoint, OneDrive or blob storage and email a link instead. The mailbox stays small and you never hit the size wall.

Method 4: Embed an Inline Image in the Email with LinkedResources

A logo in the email body isn’t a regular attachment. It goes in LinkedResources and the HTML points at it with a cid: URL. Same sample files, now with the Bayview logo on top of the invoice email:

#:package MailKit@4.18.2
using MailKit.Net.Smtp;
using MimeKit;

var builder = new BodyBuilder { TextBody = "Invoice 10482 is attached." };
var logo = builder.LinkedResources.Add(Path.Combine("Assets", "bayview-logo.png"));
logo.ContentId = MimeKit.Utils.MimeUtils.GenerateMessageId();
builder.HtmlBody = $"<img src=\"cid:{logo.ContentId}\"><p>Invoice 10482 is attached.</p>";
builder.Attachments.Add(Path.Combine("Attachments", "Invoice-10482.pdf"));

var message = new MimeMessage { Subject = "Invoice 10482 with logo" };
message.From.Add(MailboxAddress.Parse("orders@bayviewsupply.example"));
message.To.Add(MailboxAddress.Parse("marcus@harborpointcafe.example"));
message.Body = builder.ToMessageBody();

using var smtp = new SmtpClient();
await smtp.ConnectAsync("localhost", 2525);
Console.WriteLine($"Server reply: {await smtp.SendAsync(message)}");
await smtp.DisconnectAsync(true);

Output:

Server reply: Mail accepted

Invoice 10482 with logo -> marcus@harborpointcafe.example
Body: multipart/mixed
  text/plain       inline     -                            26 B
  text/html        inline     -                            80 B
  image/png        inline     bayview-logo.png           5156 B
  application/pdf  attachment Invoice-10482.pdf         43015 B
VS Code running InlineImage.cs: MailKit LinkedResources embeds bayview-logo.png as an inline image/png part with a Content-ID, next to the Invoice-10482.pdf attachment
The logo arrives as an inline image/png part, the PDF as a normal attachment

Four parts arrived: the text body, the HTML body, bayview-logo.png marked inline, and the invoice marked attachment.

How does this code work?

LinkedResources.Add returns the image part, and MimeUtils.GenerateMessageId() gives it a unique Content-ID. The HTML’s <img src="cid:..."> uses that ID, so mail apps show the logo in the body instead of as a download.

Setting both HtmlBody and TextBody gives plain-text mail apps something readable. MimeKit nests the HTML and logo inside the message, while the PDF stays a separate attachment.

Method 5: Email Attachments with System.Net.Mail MailMessage (Existing Code)

If your app already uses MailMessage, you can keep it running. Same files through the legacy classes, with the PDF from disk and a CSV from a stream:

using System.Net.Mail;
using System.Net.Mime;

using var message = new MailMessage(
    new MailAddress("orders@bayviewsupply.example", "Jenna Morales"),
    new MailAddress("marcus@harborpointcafe.example", "Marcus Reed"))
{
    Subject = "Invoice 10482 (System.Net.Mail)",
    Body = "Hi Marcus, your October invoice is attached."
};
message.Attachments.Add(new Attachment("Attachments/Invoice-10482.pdf", MediaTypeNames.Application.Pdf));
var csv = new MemoryStream("Order,Total\r\n10482,919.02\r\n"u8.ToArray());
message.Attachments.Add(new Attachment(csv, "orders.csv", "text/csv"));

using var smtp = new SmtpClient("localhost", 2525);
await smtp.SendMailAsync(message);
Console.WriteLine("Sent: " + string.Join(", ", message.Attachments.Select(x => x.Name)));

Output:

Sent: Invoice-10482.pdf, orders.csv

Invoice 10482 (System.Net.Mail) -> "Marcus Reed" <marcus@harborpointcafe.example>
Body: multipart/mixed
  text/plain       inline     -                            44 B
  application/pdf  attachment Invoice-10482.pdf         43015 B
  text/csv         attachment orders.csv                   27 B

Both attachments arrived: application/pdf for the invoice and text/csv for the 27-byte CSV.

How does this code work?

new Attachment(path, MediaTypeNames.Application.Pdf) attaches the file with an explicit type, and new Attachment(stream, name, mediaType) attaches the stream. SendMailAsync is the awaitable send.

The using var message line matters more here than in MailKit. Disposing the MailMessage disposes every attachment and the streams behind them; see Dispose and Finalize in C# for why that cleanup can’t wait for the garbage collector.

Important: keep the stream open until the send finishes. Unlike MimeKit, Attachment reads the stream during the send, not when you create it.

Test Without a Server: SpecifiedPickupDirectory

SmtpClient can also skip the network and write each message as an .eml file, which is handy in unit tests. Same files again, saved to an outbox folder:

using System.Net.Mail;

using var message = new MailMessage("orders@bayviewsupply.example",
    "marcus@harborpointcafe.example", "Invoice 10482", "Invoice attached.");
message.Attachments.Add(new Attachment("Attachments/Invoice-10482.pdf"));

Directory.CreateDirectory("outbox");
using var smtp = new SmtpClient
{
    DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory,
    PickupDirectoryLocation = Path.GetFullPath("outbox")
};
smtp.Send(message);

var eml = new DirectoryInfo("outbox").GetFiles().MaxBy(f => f.LastWriteTime)!;
Console.WriteLine($"Wrote a {eml.Length}-byte .eml file to outbox");
foreach (string line in File.ReadLines(eml.FullName))
    if (line.StartsWith("Content-")) Console.WriteLine("  " + line);

Output:

Wrote a 59781-byte .eml file to outbox
  Content-Type: multipart/mixed;
  Content-Type: text/plain; charset=us-ascii
  Content-Transfer-Encoding: quoted-printable
  Content-Type: application/octet-stream; name=Invoice-10482.pdf
  Content-Transfer-Encoding: base64
  Content-Disposition: attachment
VS Code running PickupFolder.cs: System.Net.Mail SmtpClient with SpecifiedPickupDirectory writes a 59,781-byte .eml file whose PDF attachment is application/octet-stream
No content type passed, so the PDF went out as application/octet-stream

It wrote a 59,781-byte .eml file named with a GUID, which you can open in Outlook. PickupDirectoryLocation has to be a full path, so I wrapped the folder in Path.GetFullPath.

Look at the attachment’s header, though: application/octet-stream. new Attachment(path) doesn’t look at the extension, so pass a media type whenever the recipient’s mail app should know it’s a PDF.

Directory.CreateDirectory skips the folder if it’s already there, as I explain in how to create a folder if it doesn’t exist in C#.

How Do You Set the Content Type of an Email Attachment?

MailKit guesses the type from the extension with MimeTypes.GetMimeType, and you can call it yourself to check a name before you attach it:

#:package MimeKit@4.18.2
using MimeKit;

string[] files = ["Invoice-10482.pdf", "orders-2026-10.csv", "Q4-forecast.xlsx",
                  "DeliveryPhoto-10482.jpg", "bayview-logo.png", "backup.zip", "export.dat"];
foreach (string file in files)
    Console.WriteLine($"{file,-24} {MimeTypes.GetMimeType(file)}");

Output:

Invoice-10482.pdf        application/pdf
orders-2026-10.csv       text/csv
Q4-forecast.xlsx         application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
DeliveryPhoto-10482.jpg  image/jpeg
bayview-logo.png         image/png
backup.zip               application/zip
export.dat               application/octet-stream

Known extensions map to their real types, including the long Excel one. Anything unknown, like .dat, falls back to application/octet-stream, which mail apps treat as a generic download.

When the name has no useful extension, pass the type: ContentType.Parse("application/pdf") in MailKit, or MediaTypeNames.Application.Pdf with System.Net.Mail.

Email Attachments from an ASP.NET Core Upload (IFormFile)

On a website, the file usually comes from a form. This minimal API takes an IFormFile, attaches it and emails it. It then posts the packing slip to itself so the test runs in one file:

#:sdk Microsoft.NET.Sdk.Web
#:package MailKit@4.18.2
#:property PublishAot=false
using MailKit.Net.Smtp;
using MimeKit;

var web = WebApplication.CreateBuilder(args);
web.Logging.ClearProviders();
var app = web.Build();
app.Urls.Add("http://localhost:5081");

app.MapPost("/quote-request", async (IFormFile file) =>
{
    var message = new MimeMessage { Subject = $"Quote request: {file.FileName}" };
    message.From.Add(MailboxAddress.Parse("web@bayviewsupply.example"));
    message.To.Add(MailboxAddress.Parse("orders@bayviewsupply.example"));

    var builder = new BodyBuilder { TextBody = "A customer uploaded a file." };
    await using (var upload = file.OpenReadStream())
        await builder.Attachments.AddAsync(file.FileName, upload, ContentType.Parse(file.ContentType));
    message.Body = builder.ToMessageBody();

    using var smtp = new SmtpClient();
    await smtp.ConnectAsync("localhost", 2525);
    await smtp.SendAsync(message);
    await smtp.DisconnectAsync(true);
    return $"Emailed {file.FileName} ({file.Length} bytes, {file.ContentType})";
}).DisableAntiforgery();

await app.StartAsync();                       // self-test: post a file to our own endpoint
using var http = new HttpClient();
using var form = new MultipartFormDataContent();
var pdf = new ByteArrayContent(File.ReadAllBytes("Attachments/PackingSlip-10482.pdf"));
pdf.Headers.ContentType = new("application/pdf");
form.Add(pdf, "file", "PackingSlip-10482.pdf");
var response = await http.PostAsync("http://localhost:5081/quote-request", form);
Console.WriteLine($"{(int)response.StatusCode}: {await response.Content.ReadAsStringAsync()}");
await app.StopAsync();

Output:

200: Emailed PackingSlip-10482.pdf (33030 bytes, application/pdf)

Quote request: PackingSlip-10482.pdf -> orders@bayviewsupply.example
Body: multipart/mixed
  text/plain       inline     -                            27 B
  application/pdf  attachment PackingSlip-10482.pdf     33030 B

The endpoint returned 200, and smtp4dev got the 33,030-byte PDF with the content type the browser sent.

file.OpenReadStream() hands MimeKit the upload without saving it to disk, and AddAsync copies it in. I call DisableAntiforgery() only because this demo has no form page; keep antiforgery on in a real app.

Check file.Length and the extension before you email anything a stranger uploaded. For more on the framework itself, start with the ASP.NET Core tutorials.

Common Email Attachment Errors and Fixes

These are the three errors I see most when people send attachments, each one triggered for real and caught with try-catch in C#:

#:package MailKit@4.18.2
using MailKit.Net.Smtp;
using MimeKit;

var builder = new BodyBuilder();
try { builder.Attachments.Add("Attachments/Invoice-10483.pdf"); }
catch (Exception ex) { Show(1, ex); }

using var smtp = new SmtpClient();
try { await smtp.ConnectAsync("localhost", 2526); }   // nothing listens here
catch (Exception ex) { Show(2, ex); }

File.Copy("Attachments/Invoice-10482.pdf", "copy.pdf", true);
var att = new System.Net.Mail.Attachment("copy.pdf");
try { File.Delete("copy.pdf"); }
catch (IOException ex) { Show(3, ex); }
att.Dispose();
File.Delete("copy.pdf");
Console.WriteLine($"   Deleted after Dispose(): {!File.Exists("copy.pdf")}");

void Show(int n, Exception e) => Console.WriteLine($"{n}. {e.GetType().Name}: {e.Message}");

Output:

1. FileNotFoundException: Could not find file 'C:\DotNetDemo\EmailAttachDemo\Attachments\Invoice-10483.pdf'.
2. SocketException: No connection could be made because the target machine actively refused it.
3. IOException: The process cannot access the file 'C:\DotNetDemo\EmailAttachDemo\copy.pdf' because it is being used by another process.
   Deleted after Dispose(): True
VS Code running MailErrors.cs: FileNotFoundException for a missing attachment, SocketException when no SMTP server listens, and IOException because a System.Net.Mail Attachment still holds the file
Missing file, no server, and a file locked until Dispose()
ErrorWhat it meansFix
FileNotFoundExceptionThe path is relative to the working folder, not the .cs fileBuild it with Path.Combine and check File.Exists first
SocketExceptionNothing is listening on that host and portCheck the host, port and firewall; start smtp4dev for local tests
IOException (file in use)A System.Net.Mail Attachment keeps the file openDispose the MailMessage before you move or delete the file

The third one bites cleanup jobs that email a report and then delete it. The delete failed until att.Dispose(); then it worked, which is the safe order whenever you delete a file from a folder in C#.

Real providers add one more: an AuthenticationException when the account needs OAuth 2.0 or an app password. That’s a mailbox setting, not a code bug.

Which Method Should You Use to Send Email Attachments?

SituationUseWhy
File already on diskAttachments.Add(path)Name and type come from the file
Report generated in codeAttachments.Add(name, bytes, type)No temp file to clean up
Several filesAttachments.Add in a loopCheck the encoded size, not file sizes
Logo inside the bodyLinkedResources + cid:Shows in the body, not as a download
Existing System.Net.Mail codeMailMessage + AttachmentWorks; pass a media type and dispose
Unit testssmtp4dev or SpecifiedPickupDirectoryReal MIME output, no real email

For new code, I’d pick Method 1 or 2 with MailKit and only keep System.Net.Mail where it already runs.

Things to Keep in Mind

  • Encoded size is about 37% larger than the files in my test, so leave room under the provider’s limit.
  • Pass a content type for streams and unusual extensions, or recipients get application/octet-stream.
  • Dispose MailMessage (System.Net.Mail) after the send, or the attached files stay locked.
  • Build MimeKit paths with Path.Combine: with “Attachments/Invoice-10482.pdf” on Windows, my test email named the file Attachments/Invoice-10482.pdf.
  • Never test against a real mailbox; smtp4dev on localhost shows the exact MIME parts.
  • Keep SMTP passwords and tokens in configuration or a secret store, not in source code.

Here are a few more posts that fit next to email code:

Frequently Asked Questions

Is System.Net.Mail.SmtpClient deprecated?

It isn’t marked obsolete and it still works on .NET 10, but Microsoft doesn’t recommend it for new development. The SmtpClient class page on Microsoft Learn points to MailKit instead.

How do I attach a file from memory without saving it to disk?

With MailKit, call builder.Attachments.Add("report.csv", bytes, ContentType.Parse("text/csv")) or pass a stream. With System.Net.Mail, use new Attachment(stream, "report.csv", "text/csv") and keep the stream open until the send finishes.

How many attachments can one email have?

There’s no fixed count in MailKit or System.Net.Mail. The limit is the total encoded message size your mail server accepts, and base64 makes attachments about a third larger than the files.

How do I test email sending without sending real email?

Run a local fake SMTP server such as smtp4dev and point your code at localhost. With System.Net.Mail you can also use SpecifiedPickupDirectory to write .eml files.

Why does my PDF arrive as application/octet-stream?

System.Net.Mail’s new Attachment(path) doesn’t set the type from the extension. Pass MediaTypeNames.Application.Pdf, or use MailKit, which maps .pdf to application/pdf for you.