Skip to content

fix: Use stream-based file handling and serve actual test images#57

Merged
nfMalde merged 5 commits intofeature/cid-supportfrom
copilot/sub-pr-54-another-one
Jan 4, 2026
Merged

fix: Use stream-based file handling and serve actual test images#57
nfMalde merged 5 commits intofeature/cid-supportfrom
copilot/sub-pr-54-another-one

Conversation

Copy link
Contributor

Copilot AI commented Jan 4, 2026

Addresses memory inefficiency when attaching files with explicit ContentType and improves test accuracy for URL-based image attachments.

Changes

Stream-based file attachment handling

  • Replaced File.ReadAllBytesAsync with File.OpenRead streams when creating attachments/linked resources with explicit ContentType
  • Construct MimePart directly with MimeContent(stream) to avoid loading entire files into memory
  • Added try-catch blocks to dispose streams if MimePart construction fails

Test server improvements

  • Download server now serves actual image files (TestImage.png) from TestData directory for linked resource tests
  • Maintains backward compatibility by serving synthetic "TestDownload" content for text file tests
  • Implemented directory traversal search to locate TestData path robustly across test execution contexts

Code quality

  • Extracted magic numbers to constants (MaxDirectoryTraversalLevels)
  • Simplified image extension detection using HashSet<string> with case-insensitive comparison
  • Fixed null handling for Path.GetDirectoryName() when reaching filesystem root

Example

Before (loads entire file into memory):

var data = await File.ReadAllBytesAsync(attachment.FilePath);
var linkedResource = bodyBuilder.LinkedResources.Add(fileName, data, contentType);

After (streams file content):

var fileStream = File.OpenRead(attachment.FilePath);
try {
    var linkedPart = new MimePart(contentType) {
        Content = new MimeContent(fileStream),
        // ... other properties
    };
    bodyBuilder.LinkedResources.Add(linkedPart);
} catch {
    fileStream?.Dispose();
    throw;
}

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 4 commits January 4, 2026 10:14
Co-authored-by: nfMalde <5366984+nfMalde@users.noreply.github.com>
Co-authored-by: nfMalde <5366984+nfMalde@users.noreply.github.com>
Co-authored-by: nfMalde <5366984+nfMalde@users.noreply.github.com>
Co-authored-by: nfMalde <5366984+nfMalde@users.noreply.github.com>
Copilot AI changed the title [WIP] Add CID support for inline images in emails fix: Use stream-based file handling and serve actual test images Jan 4, 2026
Copilot AI requested a review from nfMalde January 4, 2026 10:25
@nfMalde nfMalde marked this pull request as ready for review January 4, 2026 10:40
Copilot AI review requested due to automatic review settings January 4, 2026 10:40
@nfMalde nfMalde merged commit d3ef25c into feature/cid-support Jan 4, 2026
2 checks passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request improves memory efficiency when attaching files with explicit ContentType by using stream-based file handling, and enhances test accuracy by serving actual image files from the TestData directory instead of synthetic content.

Key changes:

  • Replaced File.ReadAllBytesAsync with File.OpenRead streams for file-based attachments and linked resources with explicit ContentType
  • Enhanced test download server to locate and serve actual image files from TestData directory
  • Extracted magic numbers to named constants for improved code maintainability

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/AspNetCore.MailKitMailer/Data/MailClient.cs Implements stream-based file handling for attachments and linked resources with explicit ContentType to avoid loading entire files into memory
tests/AspNetCore.MailKitMailerIntegrationTests/Abstracts/MailTestAbstracts.cs Adds directory traversal logic to locate and serve actual test images, with constants for configuration and case-insensitive image extension detection

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +351 to +368
var fileStream = File.OpenRead(attachment.FilePath);
try
{
var attachmentPart = new MimePart(contentType)
{
Content = new MimeContent(fileStream),
ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),
ContentTransferEncoding = ContentEncoding.Base64,
FileName = fileName
};
bodyBuilder.Attachments.Add(attachmentPart);
}
catch
{
// If an error occurs, ensure the stream is disposed
fileStream?.Dispose();
throw;
}
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual stream disposal in the catch block is problematic because MimeContent takes ownership of the stream and will dispose it. If the MimePart constructor or property setters succeed but an exception occurs later (e.g., when adding to Attachments), the stream will be disposed twice - once by MimeContent and once in the catch block. Use a using statement instead to ensure proper disposal only when MimePart construction fails, or rely on MimeContent to dispose the stream in all cases.

Copilot uses AI. Check for mistakes.
ContentDisposition = new ContentDisposition(ContentDisposition.Inline),
ContentTransferEncoding = ContentEncoding.Base64,
FileName = fileName,
ContentId = attachment.ContentId ?? Guid.NewGuid().ToString()
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ContentId assignment has changed behavior. The original code assigned attachment.ContentId directly (which is guaranteed to be non-null based on the isLinkedResource check at line 293). The new code adds a null-coalescing operator with a Guid fallback. This is inconsistent with how FileBytes-based linked resources are handled (line 303) and may mask bugs where ContentId is unexpectedly null. Either remove the null-coalescing operator to maintain consistency, or apply the same pattern to line 303.

Suggested change
ContentId = attachment.ContentId ?? Guid.NewGuid().ToString()
ContentId = attachment.ContentId

Copilot uses AI. Check for mistakes.
Comment on lines +149 to 153
// Fall back to creating a test file (for backward compatibility with existing tests)
filePath = Path.Combine(Directory.GetCurrentDirectory(), "inttest.txt");
File.WriteAllText(filePath, "TestDownload");

await httpContext.Response.SendFileAsync(filePath);
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback behavior creates a text file with "TestDownload" content even when an image extension is requested but the actual image file is not found. This could cause tests to pass with incorrect data (serving text content with an image extension). Consider either returning a 404 status code when an expected image file is not found, or logging a warning to make debugging easier when the TestData directory or image file cannot be located.

Copilot uses AI. Check for mistakes.
Comment on lines +322 to +340
var fileStream = File.OpenRead(attachment.FilePath);
try
{
var linkedPart = new MimePart(contentType)
{
Content = new MimeContent(fileStream),
ContentDisposition = new ContentDisposition(ContentDisposition.Inline),
ContentTransferEncoding = ContentEncoding.Base64,
FileName = fileName,
ContentId = attachment.ContentId ?? Guid.NewGuid().ToString()
};
bodyBuilder.LinkedResources.Add(linkedPart);
}
catch
{
// If an error occurs, ensure the stream is disposed
fileStream?.Dispose();
throw;
}
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual stream disposal in the catch block is problematic because MimeContent takes ownership of the stream and will dispose it. If the MimePart constructor or property setters succeed but an exception occurs later (e.g., when adding to LinkedResources), the stream will be disposed twice - once by MimeContent and once in the catch block. Use a using statement instead to ensure proper disposal only when MimePart construction fails, or rely on MimeContent to dispose the stream in all cases.

Copilot uses AI. Check for mistakes.
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.

2 participants