aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_messageview.cpp
AgeCommit message (Collapse)AuthorFilesLines
5 daysfix(pane): drop Save link from a link's context menuDanilo M.1-0/+6
Reported by hand after the item 127 fix: right-clicking a link still offered Save link. It had been deferred to item 114 alongside Save image, on the grounds that both are inert without a downloadRequested handler. That is true and it was the wrong conclusion, because the two are not the same question. Save image is content the message already carries, and item 114 is about making it work. Save link fetches a remote URL chosen by the sender, through the pane's profile, which is the one profile in this application that must never fetch remote content: that is what m_allowRemote and the interceptor exist to prevent. Answering it with a download handler would put a network fetch of attacker-controlled content behind one context-menu entry. Saving what the user actually wants already has a path that never touches the network: saveAttachment(), which writes a MIME part already parsed into memory and sanitises the filename. So it is removed rather than implemented, and the test asserts its absence. Item 114 now carries the constraint that follows: a downloadRequested handler added to make Save image work must not make Save link reachable again, which the natural per-profile implementation would do by default. Mutation checked: dropping the entry from the filter fails the test with "a link action survived: Save link". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDq53rMd3AQp7QmcZzpuBM
5 daysfix(pane): open a target="_blank" link, and drop the dead link actionsDanilo M.1-0/+124
Items 126 and 127, in one sitting because the second is only safe after the first. 126: an anchor carrying target="_blank" did nothing when clicked, with no error and nothing on screen. Chromium routes such a click to QWebEnginePage::createWindow() rather than to acceptNavigationRequest, and MessagePage did not override it, so the base implementation returned nullptr and the URL was discarded before any of our code saw it. Plain anchors were unaffected and already worked, which is why this presented as "HTML mail is broken" while a text mail's links opened: marketing HTML sets _blank on practically every anchor. createWindow() receives a WebWindowType and no URL, so an override cannot simply read the target: it arrives afterwards as a navigation on whatever page is returned. LinkRelayPage is that page. It has no view, hands the URL to the same handler the plain-link path uses, refuses the navigation, and deletes itself. Nothing is ever fetched and no second QWebEngineView is created. 127: OpenLinkInNewTab, OpenLinkInNewWindow and OpenLinkInThisWindow join removeBrowserActions()'s list. Item 100's list is the PAGE actions and was tested by right-clicking the page; these appear only over a link, so it never saw them. CopyLinkToClipboard stays, being the fallback for any link that will not open. The order matters: 126 gives the page a working createWindow(), so those entries would have stopped being dead and started opening links into a tab that does not exist. Testing needed two seams. The click cannot be synthesised, since JavaScript is off in this profile (measured: runJavaScript returns an invalid QVariant) and a synthetic press would depend on the anchor's rect and the desktop's fonts; setUrl() is no substitute because it arrives as NavigationTypeTyped. clickLinkForTest() and relayBlankTargetForTest() drive the real overrides on the real page, and setLinkOpener() substitutes a recorder for QDesktopServices::openUrl. Both routes are asserted rather than only the broken one, since they share a handler now. Three mutations checked and caught, including the filter also removing CopyLinkToClipboard, which a later sweep of "dead link actions" would otherwise take silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDq53rMd3AQp7QmcZzpuBM
6 daysfeat(pane): move the copy confirmation into the message paneDanilo M.1-7/+119
The user's preference after seeing item 115 ship: a small transient in the bottom right of the pane with a checkmark, rather than a status bar message at the far end of the window. A copy happens in the pane, so the confirmation belongs there. Three properties are load-bearing and each has a mutation that fails. The toast is a hand-placed CHILD rather than a layout item, because it floats over the message instead of taking a strip away from it: nothing reflows when it appears and the text just copied does not jump. That is why resizeEvent() is overridden, since a hand-placed child does not follow its parent. It is autoFillBackground and painted from the theme's ToolTipBase/ToolTipText, so it stays readable over a rendered message and follows the desktop theme the way the document already does. And its timer is restarted rather than started, so a second copy gets its own full reading time instead of inheriting what is left of the first. The resize test was wrong on its first draft and passed against the mutation it exists to catch. It grew the pane, which moves the right and bottom edges away, so a toast left at its old position still satisfied "inside the pane"; measured green with the reposition deleted. It shrinks now, where a stale position lands outside the new rect, which is also what the user would see. No new strings: the four messages are unchanged, only where they appear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6 daysfeat(pane): offer Select all, and report what a copy copiedDanilo M.1-0/+93
Items 115 and 117, both from the user's notes. Select all was never in Chromium's menu for this pane, measured by hand with a selection active and against a build with removeBrowserActions() reverted, so the filter is not what removed it. MessageView::addPaneActions() supplies it, static and taking the menu, mirroring removeBrowserActions() beside it. Two comments claiming the standard menu already offered it are corrected; either would have sent the next reader down the same three wrong theories the item records. The copy entries all worked and none of them said so. Four now report through the pane's existing statusMessage, each naming what it copied rather than saying "Copied", which is the item's own constraint when three of them sit together in one menu. Connected to the page's own QActions, so the report follows the entry wherever it is triggered from. The two differ in what can be tested, and the tests say so rather than papering over it. The copy path is fully covered: triggering the action runs the production path, and mutations for a duplicated message and an unwired entry both fail. addPaneActions() is covered, but showBodyContextMenu() CALLING it is not and cannot be, since createStandardContextMenu() returns nothing outside a real context-menu event; a mutation deleting that call leaves the suite green, measured. The call site is a hand test and the test file records that so nobody adds an assertion that appears to cover it. The copy strings are QT_TR_NOOP inside an array, which CLAUDE.md warns extracts nothing at file scope. Verified rather than assumed: lupdate found all four under the MessageView context, because the array sits inside a member function. 387 finished, 0 unfinished. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8 daysfix(ui): drop the browser's own actions from the message pane menuDanilo M.1-0/+80
The pane's context menu started from QWebEngineView::createStandardContextMenu() and kept it whole, so it offered Back, Forward, Reload and Save page. None of them can apply: every message is rendered with setHtml() from memory, so there is no history to go back to and nothing to reload, and the request interceptor blocks everything by default. They were inert as well as meaningless. removeBrowserActions() matches on the QAction pointer returned by page->action(), never on the entry's text, which is translated: a text match would work in English and fail in every other locale, which is a defect no test written in English would catch. Removing entries also strands separators at the edges or doubles them up, which reads as a menu that lost something, so the filter sweeps them; Qt offers nothing for this. View source is deliberately NOT filtered. It was removed with the other four at first, which was an overreach: the user asked for four and view-source has a real document and a real use. Chromium's own entry cannot work here either, since it navigates to view-source:<url> and MessagePage refuses that, so backlog item 113 implements it as our own plain-text dialog. The test builds a menu by hand, which is right for testing the filter and proves nothing about what Chromium's real menu contains. That limit is stated at the test, and is why it does not assert on SelectAll: the real menu has never offered it, verified by hand against a build with this filter reverted (backlog item 117). Backlog item 100.
11 daysrefactor(search): carry SearchMode instead of bool extendDanilo M.1-1/+1
Four signatures, no behaviour change: the two shipped operations map to Replace and Narrow. runSearchFromPane becomes a switch and gains the Exclude arm, which nothing can reach until the menu entry exists. Seven call sites across three test files moved with it, two more than the plan predicted: test_messageview and test_mainwindow also drive these signals directly. mainwindow.h and messagedetailsdialog.h now include searchterm.h for the type; messageview.h already did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
11 daysfeat(search): run a search asked for from the message paneDanilo M.1-0/+53
The panes carry a finished query and know nothing of the query bar; the window sets the field and calls the existing runner, so the account scope and the generation counter keep working as they do for a typed query. Narrowing combines here rather than in a pane, because only the window can see what the bar currently holds. The tag strip's chips join the header, the body selection and the details dialog as a fourth surface. Also fixes the details dialog to actually close when a search is chosen: the comment above the connection already described this requirement, but nothing called accept() or reject(), so the dialog stayed open, the query ran behind it, and the modal exec() never returned. This hung the whole test suite on QT_QPA_PLATFORM=offscreen once a covering test was added.
11 daysfeat(search): search for the selected body textDanilo M.1-0/+34
selectedText() reads the selection with no script injection; JavaScript stays disabled in the profile. The page's standard menu is kept and the entries are added to it below a separator. The quoting is tested through a function taking the text, so it needs no live web engine: a selection is arbitrary prose and can carry quotes, newlines and query syntax, none of which notmuch reports as an error.
11 daysfeat(search): offer the header's fields for searchingDanilo M.1-1/+98
The menu lists what is searchable rather than hit-testing which line of a rich-text label was clicked, which breaks as soon as the label wraps. The values are collected by the pass that renders the header, so nothing parses the markup back into structure. From, To and Cc appear only for a single-message thread, sharing the condition with the header's own display: a thread's recipient differs message to message, and the menu must not offer what the header is not stating. The test fixture's Date: header named the wrong weekday, which Qt::RFC2822Date rejects outright, so no date offer would have been produced from it.
2026-08-07feat(ui): fill the blank message pane with a branded placeholderDanilo M.1-0/+74
An empty right pane said nothing, and multi-select made it a routine sight. It now carries the wordmark, thread counts that run their query when clicked, and a sync line that appears only when something needs attention. Rendered into the existing web view as a third document shape, so there is one document path and one set of security rules. The brand palette is a deliberate exception to deriving colours from the desktop theme, since a logo is brand rather than chrome; the theme still picks which of the two sets is used. Counts refresh when the pane is about to show rather than in the background: one goes stale the moment a tag is edited, and refreshing one nobody is looking at is work for nothing. A generation counter discards a superseded reply, and a late answer cannot repaint over an opened thread. The helper lines are real links because JavaScript is off in this profile. The handler is gated on the placeholder actually being displayed, so the same URL inside a message body is dropped: a stranger's mail must not drive the thread list, even to run a harmless query. Three defects found while building, all silent: - Every CSS percentage was invalid. QString::arg does not collapse "%%" into "%", so the document carried "50%%" and the browser dropped each declaration holding one, disabling the mask, the glow and both radial gradients while still rendering something plausible. Substitution is by named token now, which cannot collide with a percent sign. - A geometry probe endorsed the layout while that was live, because it measured only properties without percentages. - The font test passed against a build with one face missing, since the other satisfied both of its checks on its own. The mockup's light values needed correcting against a real pane: the grid vanished at a 2% luminance step on white, and the glow subtracts light there rather than adding it, washing the pane. Strength only, not hue.
2026-08-04feat(message): show From/To/Cc and add a details dialogDanilo M.1-7/+159
MimeParser has filled To and Cc all along and HtmlBuilder simply never interpolated them, so both were parsed on every message and then discarded. The header strip showed the subject and a message count and nothing else, which is item 2 of the usability backlog. The header now adapts to what it can say honestly. A thread holding one message shows From, To and Cc under the subject, where every field is unambiguous. A thread holding several keeps showing the subject and the count alone: the recipient differs message to message, and once the user has replied there is no single address the thread is addressed to, so naming one would be a guess presented as a fact. Per-message detail is what the dialog is for. That dialog lists Subject, From, To, Cc, Date and Message-Id for every message, numbered when there is more than one, in a read-only plain-text widget. Plain text is the security decision, not a stylistic one: these values come from strangers and the dialog exists to show them verbatim, so the format that cannot interpret markup is the right one. The header label is RichText and every value interpolated into it is escaped, since an unescaped From injects into the application's own chrome rather than into the sandboxed page. Reached by a Details... button beside the subject and by Ctrl+Shift+D. Both, because a shortcut alone restates the complaint this backlog opened with. The binding is shifted because Ctrl+D is delete, and the destructive action keeps the key it already had rather than being moved to make room. An empty Cc omits its row instead of printing a label with nothing after it. Both header shapes were rendered to PNG and inspected, not only asserted. The new button also exposed a latent flaw in an older test: attachmentButtonLabels() identified attachment buttons by excluding the one other button's label, so it counted the details button as an attachment as soon as one existed. It now finds the bar by object name and reads only its children. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04feat: make attachments reachable from the message paneDanilo M.1-0/+110
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>
2026-08-04feat: own the message-pane zoom and persist itDanilo M.1-0/+55
Zoom was Chromium's, not the application's: the web view handled the keys natively and never told anyone, so there was no value to save. qtmaildir now owns it. Zoom in, out and reset are real actions, in the View menu and rebindable through [keys], and the factor is persisted to the UI state file. Ctrl+wheel over the body zooms and Ctrl+middle-click resets, both filtered by ancestry from an application-level filter: the events are delivered to an internal QQuickWidget the web view creates lazily, so a filter on the view itself never sees them. The factor is clamped to 0.5 - 3.0, and NaN, infinity, zero and negative values fall back to 1.0, since a corrupt state file must not be able to leave the pane unreadable with no visible way back. Both risks the plan flagged turned out not to exist, verified by probe rather than assumed. The application QAction wins over the web view's native zoom key, so the tracked factor cannot diverge from what is on screen. And the factor survives setHtml(), so the web view is the single source of truth and needs no reapply per render. A third finding is worth recording because it produced a wrong fix first. A probe using QTest::keyClick() reported Ctrl++ as a dead binding, and a test was written asserting that. Both were wrong: Ctrl++ is exactly what the '+' key emits on an Italian layout, confirmed against the real keyboard, and it is the shipped default. Whether a symbol needs Shift is a property of the layout, not of Qt, and keyClick() reproduces neither. The test now only checks that every default parses, and the comment in defaultBindings() says not to re-derive this from synthetic input. Ctrl+= is a second binding for reset, skipped when [keys] gives it to something else. Also fixes a pre-existing bug the new config key exposed. [general] entries were read as "general/<key>", which matches nothing: QSettings' INI backend treats a section literally named [general] as its own fallback section and strips the prefix. notmuch_config had therefore never worked. Both keys are now read without it; the file format is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04Add GPLv2-only license and per-file headersDanilo M.1-0/+18
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>
2026-08-04fix: render the message pane at allDanilo M.1-0/+161
Clicking a thread left the pane blank. Two independent bugs, both from the same false premise: that setHtml() navigates to the base URL it is given. It does not. setHtml() navigates to a data: URL carrying the markup and applies the base URL afterwards, purely as the document's origin. Verified empirically on Qt 6.11. Built on that wrong assumption were: - MessagePage::acceptNavigationRequest compared the navigation's URL against documentUrl() and rejected everything else, so the document load was refused. It now accepts a typed main-frame navigation, which is one we initiated ourselves. - RequestInterceptor exempted exactly the qtmaildir: base URL and denied everything else, so the data: document load was blocked too. The interceptor fix is scoped to ResourceTypeMainFrame rather than allowing the data: scheme outright. A blanket allow would have been a real hole: a message body can write <img src="data:..."> or an iframe, and the existing dataSchemeBlocked test in test_interceptor.cpp was right to fail when that was tried. Sub-resource data: URLs remain denied. Note this was never working. The drafted version had the same defect in a different spelling (it compared url.scheme() rather than the whole URL, and would have rejected the data: navigation just the same), and task 11 shipped with no runtime test to catch it. test_messageview.cpp now pins all three facts: the document loads, its text reaches the page, and a data: image inside a hostile body stays blocked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>