Conversation
AI-assisted: Claude Sonnet 4.6 Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
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>
| /** @var Horde_Mime_Part $part */ | ||
|
|
||
| if (!$part->isAttachment()) { | ||
| if (count($attachmentIds) === 0 && !$part->isAttachment()) { |
There was a problem hiding this comment.
Todo: Double-Check / Revert
| } | ||
| } | ||
|
|
||
| export async function fetchMessageHtmlBodyForCompose(id) { |
There was a problem hiding this comment.
Todo: Remove
first iteration had a own endpoint and therefore the new method
There was a problem hiding this comment.
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 adata-cidmarker so forwarding can restorecid: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.
| } | ||
|
|
||
| if (!empty($attachmentIds) && !in_array($part->getMimeId(), $attachmentIds, true)) { | ||
| if (!in_array($part->getMimeId(), $attachmentIds, true)) { |
There was a problem hiding this comment.
In getAttachments(), when $attachmentIds is empty (the default), the in_array($part->getMimeId(), $attachmentIds, true) check will always be false and the loop will continue for every part. This makes getAttachments() return an empty list in the common “fetch all attachments” call path. The ID filter should only run when $attachmentIds is non-empty (or invert the logic to explicitly handle the empty case).
| if (!in_array($part->getMimeId(), $attachmentIds, true)) { | |
| if (count($attachmentIds) > 0 && !in_array($part->getMimeId(), $attachmentIds, true)) { |
| $inlineAttachment = array_find($this->inlineAttachments, static function ($attachment) use ($uri) { | ||
| return $attachment['cid'] === $uri->path; | ||
| }); |
There was a problem hiding this comment.
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.
| $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; | |
| } | |
| } |
|
|
||
| import { showError, showWarning, TOAST_DEFAULT_TIMEOUT } from '@nextcloud/dialogs' | ||
| import { translate as t } from '@nextcloud/l10n' | ||
| import { generateUrl } from '@nextcloud/router' |
There was a problem hiding this comment.
generateUrl is imported but never used in this module. With the repo’s ESLint setup this is likely to fail CI (no-unused-vars). Please remove the unused import.
| import { generateUrl } from '@nextcloud/router' |
|
|
||
| // // 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']); |
There was a problem hiding this comment.
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.
| // // 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']); |
Needs #12637