Skip to content

Forward inline images#12651

Draft
kesselb wants to merge 8 commits intomainfrom
feat-forward-inline-images
Draft

Forward inline images#12651
kesselb wants to merge 8 commits intomainfrom
feat-forward-inline-images

Conversation

@kesselb
Copy link
Copy Markdown
Contributor

@kesselb kesselb commented Mar 24, 2026

Todo

Test cases

Case: Forward emails with at least two inline images

Case: Calendar invitations via mail provider are shown in evolution

The workaround added for #10416 was moved.

@kesselb kesselb self-assigned this Mar 24, 2026
}
}

export async function fetchMessageHtmlBodyForCompose(id) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Todo: Remove

first iteration had a own endpoint and therefore the new method

Copy link
Copy Markdown

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

Enables forwarding of inline images by preserving cid: ↔ attachment URL ↔ data-cid metadata across message rendering and compose, and by persisting attachment Content-ID / disposition so forwarded attachments can be reconstructed correctly.

Changes:

  • Add support for inline attachments (Content-ID + disposition) end-to-end: IMAP parsing, attachment forwarding, MIME building, and compose UI handling.
  • Extend HTML purification to rewrite cid: to attachment URLs and add a data-cid marker so forwarding can restore cid: references.
  • Add/adjust unit tests and test fixtures to cover encrypted/inline attachments, MIME building behavior, and HTMLPurifier transforms.

Reviewed changes

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

Show a summary per file
File Description
tests/data/decrypted-message-body-with-attachment.txt Adds encrypted MIME fixture with both inline image and regular attachment parts.
tests/Unit/Service/TransmissionServiceTest.php Updates tests for new LocalAttachment disposition handling.
tests/Unit/Service/MimeMessageTest.php Adds coverage for inline attachments going into multipart/related and data-cid rewrite behavior.
tests/Unit/Service/MailManagerTest.php Updates Attachment constructor usage with new fields.
tests/Unit/Service/HtmlPurify/TransformURLSchemeTest.php Adjusts tests for new inline-attachments-based CID rewrite logic.
tests/Unit/Service/HtmlPurify/TransformCidDataAttrTest.php New tests for adding data-cid based on inline attachment URLs.
tests/Unit/Service/Attachment/AttachmentServiceTest.php Updates attachment creation/forwarding tests for contentId/disposition changes.
tests/Unit/IMAP/MessageMapperTest.php Extends tests to validate content type and disposition for encrypted attachments and inline images.
tests/Unit/Db/LocalAttachmentTest.php New unit tests for LocalAttachment disposition helper method.
tests/Unit/Controller/MessagesControllerTest.php Updates Attachment constructor usage with new fields.
src/store/mainStore/actions.js Prepares both normal and inline attachments when forwarding; adds compose HTML body fetch path.
src/service/MessageService.js Adds a compose-specific HTML body fetch helper.
src/components/TextEditor.vue Allows img[data-cid] through the editor’s HTML support config.
src/components/ComposerAttachments.vue Preserves attachment type/databaseId for forwarding inline previews.
src/components/ComposerAttachment.vue Computes preview source including forwarded inline image download URLs.
lib/Service/TransmissionService.php Applies LocalAttachment disposition/contentId to MIME parts, supporting omitted Content-Disposition for iMIP.
lib/Service/MimeMessage.php Separates inline attachments into multipart/related and rewrites data-cid back to cid:.
lib/Service/MailTransmission.php Updates build() call signature for MIME message construction.
lib/Service/HtmlPurify/TransformURLScheme.php Refactors CID URL rewriting to use inline attachments list.
lib/Service/HtmlPurify/TransformCidDataAttr.php New HTMLPurifier transform to add data-cid based on inline attachment URLs.
lib/Service/Html.php Updates sanitizer to use new transforms and inline attachment URL enrichment.
lib/Service/DataUri/DataUri.php Documents data URI parameter shape for charset access.
lib/Service/Attachment/AttachmentService.php Persists contentId/disposition on forwarded attachments; supports forwarding inline message attachments.
lib/Service/AntiSpamService.php Updates build() call signature for MIME message construction.
lib/Provider/Command/MessageSend.php Infers and persists attachment disposition when provider API lacks it.
lib/Model/IMAPMessage.php Exposes inlineAttachments in full message payload; updates HTML sanitization call signature.
lib/Migration/Version5008Date20260320125737.php Adds DB columns for content_id and disposition on mail_attachments.
lib/IMAP/MessageMapper.php Adjusts attachment enumeration and attachment metadata extraction (type/disposition/content-id).
lib/Db/LocalAttachment.php Adds contentId/disposition fields and helper for disposition checks.
lib/Controller/MessagesController.php Minor formatting adjustment in HTML body retrieval path.
lib/Contracts/IAttachmentService.php Improves return type doc for getAttachment() to include ISimpleFile.
lib/Attachment.php Extends Attachment model to carry contentId/disposition; improves name/disposition normalization.
appinfo/info.xml Bumps app version.
REUSE.toml Adds REUSE annotation for the new test fixture.

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

Comment on lines +98 to +100
$inlineAttachment = array_find($this->inlineAttachments, static function ($attachment) use ($uri) {
return $attachment['cid'] === $uri->path;
});
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

array_find() is used here, but it isn’t available in the PHP versions Nextcloud Mail typically supports and there’s no polyfill in this repository. This will cause a fatal error when rewriting cid: URLs. Please replace it with a simple foreach/array_filter-based lookup that works on all supported PHP versions.

Suggested change
$inlineAttachment = array_find($this->inlineAttachments, static function ($attachment) use ($uri) {
return $attachment['cid'] === $uri->path;
});
$inlineAttachment = null;
foreach ($this->inlineAttachments as $attachment) {
if ($attachment['cid'] === $uri->path) {
$inlineAttachment = $attachment;
break;
}
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 813 to +830

// // Serve all files with a content-disposition of "attachment" to prevent Cross-Site Scripting
// $mimePart->setDisposition('attachment');
//
// // Extract headers from part
// $cdEl = $mimeHeaders['content-disposition'];
// $contentDisposition = $cdEl instanceof Horde_Mime_Headers_ContentParam
// ? array_change_key_case($cdEl->params, CASE_LOWER)
// : null;
// if (!is_null($contentDisposition) && isset($contentDisposition['filename'])) {
// $mimePart->setDispositionParameter('filename', $contentDisposition['filename']);
// } else {
// $ctEl = $mimeHeaders['content-type'];
// $contentTypeParams = $ctEl instanceof Horde_Mime_Headers_ContentParam
// ? array_change_key_case($ctEl->params, CASE_LOWER)
// : null;
// if (isset($contentTypeParams['name'])) {
// $mimePart->setContentTypeParameter('name', $contentTypeParams['name']);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

There’s a large block of commented-out legacy code left in getAttachment(). Keeping dead code in-line makes future maintenance harder and obscures the current behavior (especially around content-disposition/type handling). Please remove these commented sections now that the new implementation is in place.

Suggested change
// // Serve all files with a content-disposition of "attachment" to prevent Cross-Site Scripting
// $mimePart->setDisposition('attachment');
//
// // Extract headers from part
// $cdEl = $mimeHeaders['content-disposition'];
// $contentDisposition = $cdEl instanceof Horde_Mime_Headers_ContentParam
// ? array_change_key_case($cdEl->params, CASE_LOWER)
// : null;
// if (!is_null($contentDisposition) && isset($contentDisposition['filename'])) {
// $mimePart->setDispositionParameter('filename', $contentDisposition['filename']);
// } else {
// $ctEl = $mimeHeaders['content-type'];
// $contentTypeParams = $ctEl instanceof Horde_Mime_Headers_ContentParam
// ? array_change_key_case($ctEl->params, CASE_LOWER)
// : null;
// if (isset($contentTypeParams['name'])) {
// $mimePart->setContentTypeParameter('name', $contentTypeParams['name']);

Copilot uses AI. Check for mistakes.
@kesselb kesselb force-pushed the feat-forward-inline-images branch 3 times, most recently from 3f2700c to 4a0fcad Compare April 1, 2026 10:46
kesselb added 4 commits April 1, 2026 17:17
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
The charset item is always there

Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>

fix: Skip link check for empty message

Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
@kesselb kesselb force-pushed the feat-forward-inline-images branch from b0abe5b to bddee77 Compare April 1, 2026 15:17
@kesselb kesselb force-pushed the feat-forward-inline-images branch from 68179dc to e4a5e21 Compare April 1, 2026 16:27
* @psalm-import-type MailIMAPFullMessage from ResponseDefinitions
*
* @psalm-type InlineAttachment = array{
* id: string|null,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Todo: remove null for id, add safe-guard in fetcher. Not having a mime id is not a valid state.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants