| Age | Commit message (Collapse) | Author | Files | Lines |
|
collectParts() filed any part with a Content-Id into inlineParts and
returned before the text/plain and text/html branches. Setting a
Content-Id on the text/html body is legal and common in bulk-sender
output, and such a message parsed with both body slots empty, so
hasHtml() was false, HtmlBuilder fell through to an empty plain body,
and the pane rendered nothing. Both halves of the report, the blank
message and "no HTML part", came from that one ordering.
A content id makes a part referenceable, not undisplayable. The two are
independent. The branch now registers the part and falls through rather
than returning, so the body still fills its slot. Registering first
keeps a part that is both the body and a cid: target reachable under
its id for any sibling referencing it.
Content-Disposition is deliberately not used as the discriminator: it
is absent far more often than it is correct, and a body part commonly
carries none. The existing attachment check remains the only test for
"not a body", and the first-one-wins isEmpty() guard still stops an
inline image displacing a real body, since an image matches neither
text branch.
Verified against a hand-written fixture whose text/html part carries a
Content-Id, asserting the body renders, the id still resolves, and the
sibling image is unaffected. Load-bearing by mutation: restoring the
early return fails the test. The user could not relocate the message
that prompted the report, so the end-to-end path is unconfirmed.
Closes item 41.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The attachment bar had been an empty placeholder since it was written:
MessageView created it and added it to the layout, and nothing ever put
anything in it. MimeParser had been extracting attachments the whole
time and Attachment::saveTo() already carried the path-traversal guard,
so the backend needed calling rather than writing.
The bar holds one "Attachments (N)..." button whatever the count. One
button per file was built first and was wrong: a thread with sixteen of
them made the bar as wide as the window, pushed the splitter over and
left the thread list a few pixels wide. The button opens a dialog
listing message number, filename and size with a Save each, and a
"Save all..." when there is more than one.
Save all writes into a new subdirectory named "<date> <subject>" inside
a parent the user picks, rather than dropping sixteen files loose among
whatever is already there. Zipping was considered and rejected: Qt ships
no zip API, so a real archive meant a new build dependency or shelling
out to /usr/bin/zip at runtime, and a subdirectory answers the actual
requirement. The picker names the subfolder before the user commits to a
location.
The subject is attacker-controlled and becomes a directory name, so
attachmentFolderName() sits beside the other guards in mimeparser.cpp:
it strips separators, control characters and leading dots, caps the
length, and falls back to a generated name. Its test asserts that every
hostile subject still resolves inside the parent directory.
Two defects surfaced while using it, both silent:
saveTo() overwrites an existing file, and several messages in one thread
commonly attach the same filename. Saving that thread destroyed six of
sixteen files while reporting all sixteen as saved. The batch path now
uses saveWithoutOverwriting(), which appends " (2)" before the extension
and keeps a compound extension whole.
Qt::RFC2822Date rejects a Date header that carries a timezone comment,
"+0200 (CEST)", which is legal per RFC 5322 and common in real mail. Qt
refuses the entire string rather than ignoring the comment, so every
such message lost its date prefix. Comments are stripped before parsing.
Opening an attachment in its default application is deliberately not
included: handing a file from a stranger to xdg-open is a different
security decision from writing it where the user asked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Confirmed with the maintainer as v2-only rather than v2-or-later. LICENSE is
the official text from gnu.org. Every file under src/ and tests/ carries the
matching notice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Attachment::saveTo()'s escape guard compared paths with a bare
QString::startsWith(), which is not a path-boundary test: "/tmp/safe-evil"
textually starts with "/tmp/safe", so a sibling directory whose name merely
extends the target's name would incorrectly pass as contained within it.
Extract the check into Attachment::isPathInsideDirectory(), comparing
QDir::cleanPath()'d absolute paths and requiring an exact match or a prefix
ending at a '/' boundary. Not exploitable today since safeFilename() always
reduces the name to a bare basename before saveTo() builds the target, so
the guard is unreachable via saveTo()'s public interface; comments on both
now say so plainly instead of implying it is currently load-bearing.
Add pathInsideDirectoryRejectsSiblingPrefix, testing the guard directly
(independent of safeFilename(), which would mask a broken guard by never
producing an escaping path), and safeFilenameStripsPathComponents, testing
the sanitiser that actually stops traversal today.
|
|
|