summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/keymap.cpp5
-rw-r--r--src/mainwindow.cpp6
-rw-r--r--src/messageview.cpp115
-rw-r--r--src/messageview.h11
4 files changed, 132 insertions, 5 deletions
diff --git a/src/keymap.cpp b/src/keymap.cpp
index d8aacc5..b18f5d5 100644
--- a/src/keymap.cpp
+++ b/src/keymap.cpp
@@ -36,6 +36,7 @@ QStringList KeyMap::knownActions()
QStringLiteral("complete_query"),
QStringLiteral("toggle_html"),
QStringLiteral("load_remote"),
+ QStringLiteral("message_details"),
QStringLiteral("zoom_in"),
QStringLiteral("zoom_out"),
QStringLiteral("zoom_reset"),
@@ -71,6 +72,10 @@ QList<QPair<QString, QString>> KeyMap::defaultBindings()
{ QStringLiteral("Ctrl+Space"), QStringLiteral("complete_query") },
{ QStringLiteral("Ctrl+H"), QStringLiteral("toggle_html") },
{ QStringLiteral("Ctrl+M"), QStringLiteral("load_remote") },
+ // Shifted because Ctrl+D is delete. Both are "D for details/delete"
+ // words, and the destructive one keeps the unshifted key it already
+ // had rather than being moved to make room.
+ { QStringLiteral("Ctrl+Shift+D"), QStringLiteral("message_details") },
// Ctrl++ is what the '+' key really delivers on a layout where '+' is
// unshifted, an Italian one among them, confirmed against the actual
// keyboard. QTest::keyClick() cannot reproduce it, so a synthetic-input
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 0f169d9..a0ae7c4 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -458,6 +458,11 @@ void MainWindow::registerActions()
tr("Load remote images for the current thread"), [this]() {
m_messageView->loadRemoteContent();
});
+ addAction(QStringLiteral("message_details"), tr("Message &details"),
+ tr("Show the full headers of every message in the thread"),
+ [this]() {
+ m_messageView->showDetailsDialog();
+ });
addAction(QStringLiteral("zoom_in"), tr("Zoom &in"),
tr("Enlarge the message text"), [this]() {
m_messageView->zoomIn();
@@ -539,6 +544,7 @@ void MainWindow::buildMenus()
viewMenu->addSeparator();
viewMenu->addAction(m_actions.value(QStringLiteral("toggle_html")));
viewMenu->addAction(m_actions.value(QStringLiteral("load_remote")));
+ viewMenu->addAction(m_actions.value(QStringLiteral("message_details")));
viewMenu->addSeparator();
viewMenu->addAction(m_actions.value(QStringLiteral("zoom_in")));
viewMenu->addAction(m_actions.value(QStringLiteral("zoom_out")));
diff --git a/src/messageview.cpp b/src/messageview.cpp
index 4423791..d7167bf 100644
--- a/src/messageview.cpp
+++ b/src/messageview.cpp
@@ -22,6 +22,8 @@
#include <QDesktopServices>
#include <QDialog>
#include <QDialogButtonBox>
+#include <QFontDatabase>
+#include <QPlainTextEdit>
#include <QDir>
#include <QFileDialog>
#include <QHBoxLayout>
@@ -124,6 +126,22 @@ MessageView::MessageView(QWidget *parent)
m_headerLabel->setWordWrap(true);
m_headerLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
+ // To the right of the header, per the user's decision: the summary answers
+ // "who is this from", this answers "what actually happened to it". A button
+ // and not only a shortcut, since "everything needs a memorized key" is the
+ // complaint this whole backlog started from.
+ m_detailsButton = new QPushButton(tr("Details..."), this);
+ m_detailsButton->setObjectName(QStringLiteral("messageDetails"));
+ m_detailsButton->setToolTip(tr("Show the full headers of every message"));
+ connect(m_detailsButton, &QPushButton::clicked,
+ this, &MessageView::showDetailsDialog);
+ m_detailsButton->hide();
+
+ auto *headerRow = new QHBoxLayout;
+ headerRow->addWidget(m_headerLabel, 1);
+ // Top-aligned so it stays put as the header grows to four rows.
+ headerRow->addWidget(m_detailsButton, 0, Qt::AlignTop);
+
m_blockedLabel = new QLabel(tr("Remote content blocked"), this);
m_loadRemoteButton = new QPushButton(tr("Load remote content"), this);
connect(m_loadRemoteButton, &QPushButton::clicked,
@@ -135,6 +153,7 @@ MessageView::MessageView(QWidget *parent)
blockedRow->addStretch();
m_attachmentBar = new QWidget(this);
+ m_attachmentBar->setObjectName(QStringLiteral("attachmentBar"));
new QHBoxLayout(m_attachmentBar);
// Tags live under the message rather than in the thread list, where
@@ -143,7 +162,7 @@ MessageView::MessageView(QWidget *parent)
m_tagStrip->hide();
auto *layout = new QVBoxLayout(this);
- layout->addWidget(m_headerLabel);
+ layout->addLayout(headerRow);
layout->addLayout(blockedRow);
layout->addWidget(m_view, 1);
layout->addWidget(m_attachmentBar);
@@ -260,17 +279,103 @@ void MessageView::updateHeader()
{
if (m_items.isEmpty()) {
m_headerLabel->clear();
+ m_detailsButton->hide();
return;
}
+ m_detailsButton->show();
+
// The thread's subject comes from its first message; later replies carry
// Re: prefixes that add nothing.
const QString subject = m_items.first().message.subject;
- m_headerLabel->setText(
- QStringLiteral("<b>%1</b><br><small>%2</small>")
- .arg(subject.toHtmlEscaped(),
- tr("%n message(s) in thread", "", m_items.size())));
+ QString text = QStringLiteral("<b>%1</b>").arg(subject.toHtmlEscaped());
+
+ // The header adapts to what it can say honestly. From, To and Cc are
+ // per-message, and the pane shows a whole thread, so they are only
+ // unambiguous when the thread holds exactly one message. For a real thread
+ // the recipient differs message to message (once the user replies, one is
+ // addressed to them and the next to the other party), and neither the union
+ // nor the intersection is "the" recipient. Rather than pick one or compute
+ // a participants list, the thread case says only the subject and the count,
+ // and the per-message detail belongs to the dialog.
+ if (m_items.size() == 1) {
+ const ParsedMessage &message = m_items.first().message;
+
+ // Every value here is attacker-controlled and the label is RichText, so
+ // escaping is not cosmetic: an unescaped From injects markup into the
+ // application's own chrome rather than into the sandboxed page.
+ auto row = [&text](const QString &label, const QString &value) {
+ if (value.isEmpty())
+ return; // An empty row reads as a rendering fault.
+ text += QStringLiteral("<br><small>%1 %2</small>")
+ .arg(label.toHtmlEscaped(), value.toHtmlEscaped());
+ };
+
+ row(tr("From:"), message.from);
+ row(tr("To:"), message.to);
+ row(tr("Cc:"), message.cc);
+ } else {
+ text += QStringLiteral("<br><small>%1</small>")
+ .arg(tr("%n message(s) in thread", "", m_items.size()));
+ }
+
+ m_headerLabel->setText(text);
+}
+
+void MessageView::showDetailsDialog()
+{
+ if (m_items.isEmpty())
+ return;
+
+ QDialog dialog(this);
+ dialog.setWindowTitle(tr("Message details"));
+
+ auto *layout = new QVBoxLayout(&dialog);
+
+ auto *details = new QPlainTextEdit(&dialog);
+ details->setReadOnly(true);
+ // A monospaced font keeps a long Received chain readable as the wrapped
+ // record it is.
+ details->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
+ details->setLineWrapMode(QPlainTextEdit::NoWrap);
+
+ // setPlainText, and a QPlainTextEdit rather than a label: this dialog shows
+ // header values verbatim, and those come from strangers. Plain text cannot
+ // interpret markup, so there is nothing here to escape and nothing that
+ // could render.
+ QString text;
+ for (int i = 0; i < m_items.size(); ++i) {
+ const ParsedMessage &message = m_items.at(i).message;
+
+ if (i > 0)
+ text += QLatin1Char('\n');
+ if (m_items.size() > 1)
+ text += tr("--- Message %1 of %2 ---")
+ .arg(i + 1).arg(m_items.size()) + QLatin1Char('\n');
+
+ auto line = [&text](const QString &label, const QString &value) {
+ if (!value.isEmpty())
+ text += label + QLatin1Char(' ') + value + QLatin1Char('\n');
+ };
+
+ line(tr("Subject:"), message.subject);
+ line(tr("From:"), message.from);
+ line(tr("To:"), message.to);
+ line(tr("Cc:"), message.cc);
+ line(tr("Date:"), message.date);
+ line(tr("Message-Id:"), message.messageId);
+ }
+ details->setPlainText(text);
+
+ layout->addWidget(details);
+
+ auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dialog);
+ connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
+ layout->addWidget(buttons);
+
+ dialog.resize(700, 400);
+ dialog.exec();
}
void MessageView::render()
diff --git a/src/messageview.h b/src/messageview.h
index 244a1ba..1be17f8 100644
--- a/src/messageview.h
+++ b/src/messageview.h
@@ -65,6 +65,15 @@ public:
/// Tags of the thread on display, shown as chips along the bottom.
void setTags(const QStringList &tags);
+ /// The full headers of every message in the thread, read-only. Also
+ /// reachable from the button beside the header; public so the window's
+ /// message_details action can call it.
+ ///
+ /// Plain text, not rich: header values are attacker-controlled and this
+ /// dialog exists to show them verbatim, so the format that cannot
+ /// interpret markup is the right one.
+ void showDetailsDialog();
+
/// The body zoom factor. Chromium's own range is roughly 0.25 to 5.0;
/// these are tighter, since a pane at either extreme is unusable and the
/// only visible way back is a menu entry the user cannot read.
@@ -126,6 +135,7 @@ private:
/// Every attachment in the thread, in the order the messages render.
QList<Attachment> allAttachments() const;
+
QList<ThreadRenderItem> m_items;
bool m_preferHtml = true;
@@ -137,6 +147,7 @@ private:
QLabel *m_headerLabel = nullptr;
QLabel *m_blockedLabel = nullptr;
QPushButton *m_loadRemoteButton = nullptr;
+ QPushButton *m_detailsButton = nullptr;
QWidget *m_attachmentBar = nullptr;
TagStrip *m_tagStrip = nullptr;
};