fix: Use stream-based file handling and serve actual test images#57
fix: Use stream-based file handling and serve actual test images#57nfMalde merged 5 commits intofeature/cid-supportfrom
Conversation
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>
There was a problem hiding this comment.
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.ReadAllBytesAsyncwithFile.OpenReadstreams 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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| ContentDisposition = new ContentDisposition(ContentDisposition.Inline), | ||
| ContentTransferEncoding = ContentEncoding.Base64, | ||
| FileName = fileName, | ||
| ContentId = attachment.ContentId ?? Guid.NewGuid().ToString() |
There was a problem hiding this comment.
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.
| ContentId = attachment.ContentId ?? Guid.NewGuid().ToString() | |
| ContentId = attachment.ContentId |
| // 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); |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
Addresses memory inefficiency when attaching files with explicit ContentType and improves test accuracy for URL-based image attachments.
Changes
Stream-based file attachment handling
File.ReadAllBytesAsyncwithFile.OpenReadstreams when creating attachments/linked resources with explicit ContentTypeMimePartdirectly withMimeContent(stream)to avoid loading entire files into memoryTest server improvements
Code quality
MaxDirectoryTraversalLevels)HashSet<string>with case-insensitive comparisonPath.GetDirectoryName()when reaching filesystem rootExample
Before (loads entire file into memory):
After (streams file content):
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.