Skip to content

fix(smtp): embed inline attachments as linked resources - #47

Open
donwibier wants to merge 1 commit into
jcamp-code:mainfrom
donwibier:fix/smtp-inline-attachments
Open

donwibier wants to merge 1 commit into
jcamp-code:mainfrom
donwibier:fix/smtp-inline-attachments

Conversation

@donwibier

Copy link
Copy Markdown

Problem

SmtpSender adds every attachment as a System.Net.Mail.Attachment, even when IsInline = true and a ContentId is set:

var email = Email.From("from@example.com")
    .To("to@example.com")
    .Subject("Test")
    .Body("<p><img src=\"cid:logo\"></p>", isHtml: true)
    .Attach(new Attachment
    {
        Data = logoStream,
        Filename = "logo.png",
        ContentType = "image/png",
        ContentId = "logo",
        IsInline = true
    });

The message is sent as multipart/mixed and the image gets Content-Disposition: attachment:

Content-Type: multipart/mixed;
Content-Type: text/html; charset=utf-8
Content-Type: image/png; name=logo.png
Content-Disposition: attachment
Content-ID: <logo>

Mail clients then treat the image as an attachment. Thunderbird shows it only as an attachment, not in the body; Outlook shows it in the body and also as an attachment.

This has been reported on the original repository, see lukencode#101, with open PRs lukencode#276 and lukencode#325, and is still present in this fork.

Fix

Inline attachments that have a ContentId are added as LinkedResources of the HTML AlternateView, which produces the multipart/related structure mail clients expect for embedded images:

  • With a plaintext alternative: the existing HTML AlternateView gets the linked resources.
  • Without a plaintext alternative: an HTML body that has inline attachments is put into an AlternateView so the resources can be linked to it.
  • Unchanged: regular attachments, plain-text bodies, and inline attachments without a ContentId.

The same message is now sent as:

Content-Type: multipart/related;
Content-Type: text/html; charset=utf-8
Content-Type: image/png; name=logo.png
Content-ID: <logo>

With a regular attachment next to it, the structure is multipart/mixed containing the multipart/related HTML part and the attachment.

Tests

Three new tests in SmtpSenderTests write the message to a pickup directory and check the MIME structure of the .eml:

  • InlineAttachmentIsEmbeddedInHtmlBody: multipart/related, Content-ID: <logo>, no attachment disposition.
  • InlineAttachmentIsEmbeddedInHtmlAlternateView: the same with a plaintext alternative, inside multipart/alternative.
  • RegularAttachmentStaysAttachmentNextToInlineAttachment: multipart/mixed; only the regular attachment has Content-Disposition: attachment.

All three fail without the fix and pass with it. The full test suite passes on net8.0, net9.0 and net10.0. The same approach is running in production for us, and the resulting e-mails were checked in Thunderbird and Outlook: the image shows only in the body.

Note

Building locally currently fails with NU1902 (a known vulnerability advisory for the MailKit 4.14.1 dependency, treated as an error because of TreatWarningsAsErrors). That is unrelated to this change; I ran the tests with -p:NuGetAudit=false.

SmtpSender added every attachment as a System.Net.Mail.Attachment, even
when IsInline is true and a ContentId is set. The message then goes out
as multipart/mixed with "Content-Disposition: attachment", so images
referenced from the HTML body with <img src="cid:..."> are shown as
separate attachments (Thunderbird shows them only as an attachment,
Outlook shows them inline and as an attachment).

Inline attachments with a ContentId are now added as LinkedResources of
the HTML AlternateView, which produces multipart/related. When there is
no plaintext alternative, an HTML body with inline attachments is put
into an AlternateView for this. Regular attachments, plain-text bodies
and inline attachments without a ContentId are unchanged.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
27.7% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 13 complexity · 5 duplication

Metric Results
Complexity 13
Duplication 5

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant