diff options
| author | saturneric <[email protected]> | 2026-09-14 20:55:10 +0200 |
|---|---|---|
| committer | saturneric <[email protected]> | 2026-09-14 20:55:10 +0200 |
| commit | c4e4c6d249df01d0526eadd6c3565c591d209b90 (patch) | |
| tree | 710df050b9a7a5e620d1bb177f032eda6cd3b70d /src | |
| parent | feat(email): add encrypted recipient capability models (diff) | |
| download | modules-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.cpp | 121 | ||||
| -rw-r--r-- | src/m_email/EMailPageView.h | 11 |
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 |
