The IMAP fetch path silently dropped every attachment after the first
because downloadAttachment was hard-coded to index [0]. Email messages
with N>1 attachments lost (N-1) files with no warning logged.
Fix:
- Loop through every detected attachment and download each one.
- Expose all successfully downloaded files under metadata.files for
callers that need access beyond the first.
- Keep file_info pointing at the first attachment so the existing
FetchHandler pipeline contract (single file_info per item) is
preserved.
- Log a warning for each attachment whose download fails (e.g. empty
body, decode failure, disk write failure) and surface the count via
metadata.attachments_skipped so silent loss is no longer possible.
- Include the on-disk filename in the file_info struct so callers can
distinguish multiple files in metadata.files.
Summary
Fixes #2078 — silent data loss when an IMAP message has more than one attachment.
The bug
FetchEmailAbility::fetchMessage()hard-coded\$attachments[0]when callingdownloadAttachment(), so only the first attachment was ever written to disk. Every additional attachment was silently dropped:ERRORorWARNINGrow in the logsmetadata.attachment_countcorrectly reported the upstream count (2, 3, etc.) — making it look like everything workedReproducer (on extrachill.com production, against the email used to discover this):
The fix
[0].metadata.filesso callers (CLI/JSON consumers, downstream abilities) can read past the first attachment.\$item['file_info']pointing at the first attachment so the existing single-file_info-per-item contract enforced byFetchHandler::toDataPackets()continues to hold. Nothing in the generic pipeline contract changes.warningfor each attachment whose download fails (empty body, decode failure, disk write failure) and surface the count viametadata.attachments_skippedso silent loss can no longer happen.file_infostruct so callers can distinguish multiple files insidemetadata.files.What did NOT change
FetchHandler::toDataPackets()still reads\$item['file_info']as a single struct and produces one packet per item. Email continues to emit one item per message; the extra attachments simply become accessible viametadata.filesinstead of being lost.wp_unique_filename()already handled collisions correctly insidedownloadAttachment(); the bug was never about collisions, it was about the call site only ever firing once.Why one-item-per-email, not one-item-per-attachment
Each email has one subject, one body, and one logical content payload. Splitting into N items would duplicate the body text N times and break dedupe via
item_identifier(which is the message-id). The N-attachments-on-one-item shape is consistent with how an email is modeled everywhere else in the system.Test plan
On a live install with at least one multi-attachment email in the inbox:
For a synthetic test, send yourself a 3-attachment email and walk through the same steps. All three files should land on disk;
metadata.filesshould have length 3;file_infoshould equalmetadata.files[0].Logging changes
New WARNING entries get a context payload
{ uid, filename, part_number }so the operator can correlate them to the source email. Existing INFO/DEBUG output is unchanged.Closes
Closes #2078