Embedding Local Images for Email Clients
A two-person team is building a desktop CRM. One of the headline features is an outbound email composer: the salesperson drafts a follow-up to a lead, drops in screenshots from the meeting, hits Send, and the message lands in the prospect's inbox. The composer uses the WinForms HTML Editor as its body surface, and the team writes a simple "send" path that grabs the editor's body HTML and shoves it into a MailMessage:
var msg = new MailMessage();
msg.IsBodyHtml = true;
msg.Body = htmlEditor1.Content.GetBodyHtml(true);
msg.From = new MailAddress(salesperson.Email);
msg.To.Add(lead.Email);
msg.Subject = "Following up on our chat";
new SmtpClient(...).Send(msg);They send a test to a personal Gmail account. The email arrives. Where the screenshots should be, there are three small broken-image icons. Opening the source reveals the obvious problem: the body still contains <img src="C:\Users\ana\Pictures\meeting-1.png">. Gmail, naturally, has no idea where Ana's Pictures folder is.
The proper format for embedded images in email
Email clients have rendered embedded images for over twenty years using the same recipe: a multipart/related MIME body (RFC 2387). The HTML lives in one part, every image lives in its own part with a Content-ID header, and the HTML references each image by cid: URI: <img src="cid:meeting-1@cidpool">. Every major client -- Outlook, Gmail, Apple Mail, Thunderbird, every mobile client -- renders this correctly without prompting the user about external content.
Building that structure by hand from a string of HTML is fiddly. The editor ships a helper that does the whole transformation in one call.

The helper
On editor.Content (an IContentService):
MailMessage GetEmailMessageWithLocalImagesEmbedded();The helper walks the document, finds every <img> whose src points at a local file, reads the file bytes, generates a fresh Content-ID, attaches the image as a LinkedResource under an AlternateView of type text/html, and rewrites the src in the body HTML to cid:<the-id>. Remote URLs (http://, https://) are passed through untouched -- the recipient's client will fetch them at display time as it always has.
The returned MailMessage has IsBodyHtml = true, the rewritten body, and the AlternateView with all linked resources. From, To, and Subject are left for the caller to fill in.
The fixed send path
using System.Net;
using System.Net.Mail;
private void btnSend_Click(object sender, EventArgs e)
{
// 1. Ask the editor to build a multipart/related message.
// Local <img src="C:\..."> become cid:<generated-id> automatically.
using MailMessage msg = htmlEditor1.Content.GetEmailMessageWithLocalImagesEmbedded();
// 2. Fill in the headers the helper does not own.
msg.From = new MailAddress(salesperson.Email, salesperson.DisplayName);
msg.To.Add(new MailAddress(lead.Email));
msg.Subject = txtSubject.Text;
// 3. Hand it to SMTP. Standard SmtpClient setup -- the
// MailMessage object has all the embedding wiring already.
using var smtp = new SmtpClient("smtp.crmco.com", 587)
{
EnableSsl = true,
Credentials = new NetworkCredential(smtpUser, smtpPass)
};
smtp.Send(msg);
}Ana sends the same test again. The screenshots arrive in Gmail. No broken icons, no "external content blocked" banner, no surprise.

Refinement -- mixed local and remote, and re-sends
The composer is also used for newsletter blasts where the brand banner is hosted on the company's CDN. That image stays a normal HTTP URL; only Ana's local screenshots become CID parts. The helper handles the mix cleanly without any extra configuration.
The helper does not mutate the editor's in-memory document. Ana can hit Send, decide the body wasn't quite right, edit a paragraph, and hit Send again -- each call regenerates a fresh MailMessage with fresh Content-IDs from the current state of the document. Calls are independent.
If the destination is not SMTP
The "embed images" requirement comes up in two distinct shapes. The email shape is what this page is about. The other shape -- "I want a single self-contained HTML string to save to a database column or to a .html file" -- has a different helper. That one is EmbedLocalImagesAsBase64(), also on editor.Content, and it rewrites src attributes to Base64 data URIs in place. See Embedding Local Images using Data URIs. Picking the wrong one inflates the email by 33 percent for no benefit (data URIs are not how email embeds work) or leaves a database row with no images attached (CID references are meaningless outside a MIME envelope). Two helpers, two jobs.