aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorsaturneric <[email protected]>2026-09-14 20:55:10 +0200
committersaturneric <[email protected]>2026-09-14 20:55:10 +0200
commitc4e4c6d249df01d0526eadd6c3565c591d209b90 (patch)
tree710df050b9a7a5e620d1bb177f032eda6cd3b70d /src
parentfeat(email): add encrypted recipient capability models (diff)
downloadmodules-c4e4c6d249df01d0526eadd6c3565c591d209b90.tar.gz
modules-c4e4c6d249df01d0526eadd6c3565c591d209b90.zip
feat(email): show accurate decryption capability
- inspect encrypted recipient keys instead of relying on message addresses - distinguish readable, unreadable, and unknown decryption states - refresh locked message status after keyring changes - name reply and forward tabs from their sanitized subjects
Diffstat (limited to 'src')
-rw-r--r--src/m_email/EMailPageView.cpp121
-rw-r--r--src/m_email/EMailPageView.h11
2 files changed, 123 insertions, 9 deletions
diff --git a/src/m_email/EMailPageView.cpp b/src/m_email/EMailPageView.cpp
index ccec83e..65b1269 100644
--- a/src/m_email/EMailPageView.cpp
+++ b/src/m_email/EMailPageView.cpp
@@ -1574,6 +1574,23 @@ void EMailNotifyKeyringChanged() {
}
void EMailPageView::NotifyKeyringChanged() {
+ // Whether this message can be opened is a statement ABOUT the key database,
+ // so it stops being true the moment that database changes: import the secret
+ // key it was encrypted to, or switch to a profile that holds it, and
+ // "cannot be opened on this computer" is simply wrong.
+ //
+ // Recomputed rather than discarded, unlike the verification below. This
+ // costs one packet read and one key lookup, runs no crypto and asks for no
+ // passphrase, so there is nothing to make the user ask for a second time.
+ // It also has to happen before the early return: an encrypted message that
+ // carries no signature has no regions at all, and that is precisely the
+ // message whose locked panel is on screen.
+ if (locked_capability_ != nullptr &&
+ (security_state_ == EMailSecurityState::kENCRYPTED ||
+ security_state_ == EMailSecurityState::kSIGNED_ENCRYPTED)) {
+ refresh_locked_panel();
+ }
+
if (regions_.isEmpty()) return;
// The previous answer was about a keyring that no longer exists: a signature
@@ -1776,11 +1793,18 @@ void EMailPageView::slot_derive_message(int mode) {
return;
}
+ // Name the tab after what it is. The subject already carries the "Re: " or
+ // "Fwd: " prefix, so a reply is told apart from its original at a glance,
+ // which "untitled.eml" never allowed. The host does not sanitize the title
+ // it is handed, so it is sanitized here.
+ auto title = TabTitleForSubject(derived.subject);
+ if (title.isEmpty()) title = tr("untitled.eml");
+
QWidget* page = nullptr;
QMetaObject::invokeMethod(
edit, "SlotNewCustomTab", Qt::DirectConnection,
Q_RETURN_ARG(QWidget*, page), Q_ARG(QString, "email"),
- Q_ARG(QString, "untitled.eml"), Q_ARG(QIcon, QIcon(":/icons/email.png")),
+ Q_ARG(QString, title), Q_ARG(QIcon, QIcon(":/icons/email.png")),
Q_ARG(QString, ":/icons/email.png"));
if (page == nullptr) {
@@ -1901,12 +1925,95 @@ void EMailPageView::refresh_locked_panel() {
}
void EMailPageView::refresh_locked_capability(const QStringList& named) {
- // The addresses this user holds the secret half for. An address being here
- // is not a promise the decryption will work -- the message may be encrypted
- // to a key whose address the headers never mention, which is exactly why
- // Decrypt stays enabled either way -- but its ABSENCE across every named
- // recipient is worth saying before the user presses a button that cannot
- // succeed.
+ // What the MESSAGE says, not what the headers say. A message is encrypted to
+ // a key, and the key it names is usually an encryption subkey whose UID
+ // address need never appear in To or Cc -- so matching addresses answered a
+ // different question than the one asked, and answered it wrongly in both
+ // directions: it claimed a readable message could not be opened whenever the
+ // key's address differed from the header, and claimed the user held the key
+ // for mail encrypted to someone else who happened to share an address.
+ const auto* ciphertext = FindEncryptedCiphertextPart(tree_root_);
+ if (ciphertext != nullptr) {
+ GFGpgEncRecipient* raw = nullptr;
+ int count = 0;
+ if (GFGpgSniffEncryptedRecipients(
+ GFGpgCurrentGpgContextChannel(), ciphertext->data.constData(),
+ static_cast<int>(ciphertext->data.size()), &raw, &count) == 0) {
+ QList<EMailEncRecipient> recipients;
+ recipients.reserve(count);
+ for (int i = 0; i < count; ++i) {
+ // Copied rather than taken: these strings belong to the array that
+ // GFGpgFreeEncRecipients releases as a whole.
+ recipients.append(EMailEncRecipient{
+ QString::fromUtf8(raw[i].key_id),
+ QString::fromUtf8(raw[i].pub_algo),
+ QString::fromUtf8(raw[i].fingerprint),
+ QString::fromUtf8(raw[i].uid),
+ raw[i].key_found != 0,
+ raw[i].has_secret != 0,
+ raw[i].hidden != 0,
+ });
+ }
+ GFGpgFreeEncRecipients(raw, count);
+
+ if (!recipients.isEmpty()) {
+ show_decrypt_capability(DescribeDecryptCapability(recipients));
+ return;
+ }
+ }
+ }
+
+ // Nothing could be read out of the message itself -- it is not an RFC 3156
+ // encrypted message, or the engine could not parse its packets. Fall back to
+ // the addresses, which is a guess, and worded as one.
+ refresh_locked_capability_by_address(named);
+}
+
+void EMailPageView::show_decrypt_capability(
+ const EMailDecryptCapability& capability) {
+ locked_capability_->setVisible(true);
+
+ switch (capability.verdict) {
+ case EMailDecryptVerdict::kCAN_OPEN:
+ EMailSetLabelColor(locked_capability_, AccentColor(this, true));
+ locked_capability_->setText(
+ capability.holding.size() == 1
+ ? tr("You hold the private key for %1, so this message can be "
+ "opened here.")
+ .arg(capability.holding.front())
+ : tr("You hold private keys for %1, so this message can be "
+ "opened here.")
+ .arg(capability.holding.join(", ")));
+ return;
+
+ case EMailDecryptVerdict::kCANNOT_OPEN:
+ EMailSetLabelColor(locked_capability_,
+ ThemeColor(this, &GFUIWarningColor));
+ locked_capability_->setText(
+ tr("This message is encrypted to keys you do not hold the private "
+ "half of, so it cannot be opened on this computer."));
+ return;
+
+ case EMailDecryptVerdict::kUNKNOWN:
+ EMailSetLabelColor(locked_capability_, MutedColor(this));
+ locked_capability_->setText(
+ capability.has_hidden
+ ? tr("Some recipients of this message were deliberately not "
+ "named, so there is no way to tell whether you can open it "
+ "until you try.")
+ : tr("The recipients of this message cannot be read, so there is "
+ "no way to tell whether you can open it until you try."));
+ return;
+ }
+}
+
+void EMailPageView::refresh_locked_capability_by_address(
+ const QStringList& named) {
+ // The addresses this user holds the secret half for. A far weaker signal
+ // than the recipient key ids above -- it can only ever be a guess, because
+ // the key a message is encrypted to need not carry any of these addresses --
+ // so it is reached only when the message itself could not be read, and every
+ // wording below is hedged accordingly.
char** addresses = nullptr;
int count = 0;
if (GFGpgListKeyAddresses(GFGpgCurrentGpgContextChannel(), 1, &addresses,
diff --git a/src/m_email/EMailPageView.h b/src/m_email/EMailPageView.h
index 72810d5..34f47a4 100644
--- a/src/m_email/EMailPageView.h
+++ b/src/m_email/EMailPageView.h
@@ -374,9 +374,16 @@ class EMailPageView : public QWidget {
void open_attachment(QTreeWidgetItem* item);
/// Hands @p att to the host's key import.
void import_attachment_key(const EMailAttachment& att);
- /// Says whether this user holds a private key for any of @p named, so the
- /// encrypted panel can answer "can I open this" before the user tries.
+ /// Says whether this message can be opened here, so the encrypted panel can
+ /// answer "can I open this" before the user tries. Reads the recipient key
+ /// ids out of the ciphertext and asks the key database for them; @p named is
+ /// only used for the address-matching fallback below.
void refresh_locked_capability(const QStringList& named);
+ /// Renders one verdict from refresh_locked_capability().
+ void show_decrypt_capability(const EMailDecryptCapability& capability);
+ /// The guess of last resort, for a message whose own recipients could not be
+ /// read: does this user hold a private key for any of @p named?
+ void refresh_locked_capability_by_address(const QStringList& named);
/// Every OpenPGP key part carried by the parsed message, in tree order.
[[nodiscard]] auto message_key_parts() const -> QList<const EMailPart*>;
/// Imports all of them, then asks the Security tab to describe the keyring