cli/show: add information about which headers were protected
authorDaniel Kahn Gillmor <dkg@fifthhorseman.net>
Mon, 27 May 2019 22:14:16 +0000 (18:14 -0400)
committerDavid Bremner <david@tethera.net>
Wed, 29 May 2019 11:11:50 +0000 (08:11 -0300)
The header-mask member of the per-message crypto object allows a
clever UI frontend to mark whether a header was protected (or not).
And if it was protected, it contains enough information to show useful
detail to an interested user.  For example, an MUA could offer a "show
what this message's Subject looked like on the wire" feature in expert
mode.

As before, we only handle Subject for now, but we might be able to
handle other headers in the future.

Signed-off-by: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
Amended by db: tweaked schemata notation.

devel/schemata
notmuch-show.c
test/T356-protected-headers.sh

index 72feb7b767df254a84a20cf1c0bb7fa06a955cf7..28332c6bd1d9b0de96db2be77b695183e6a896df 100644 (file)
@@ -4,9 +4,9 @@ format (currently JSON and S-Expressions).
 []'s indicate lists.  List items can be marked with a '?', meaning
 they are optional; or a '*', meaning there can be zero or more of that
 item.  {}'s indicate an object that maps from field identifiers to
-values.  An object field marked '?' is optional.  |'s indicate
-alternates (e.g., int|string means something can be an int or a
-string).
+values.  An object field marked '?' is optional; one marked with '*'
+can repeat (with a different name). |'s indicate alternates (e.g.,
+int|string means something can be an int or a string).
 
 For S-Expression output, lists are printed delimited by () instead of
 []. Objects are printed as p-lists, i.e. lists where the keys and values
@@ -48,6 +48,9 @@ threadid = string
 # Message ID, sans "id:"
 messageid = string
 
+# E-mail header name, sans trailing colon, like "Subject" or "In-Reply-To"
+header_name = string
+
 notmuch show schema
 -------------------
 
@@ -88,9 +91,15 @@ crypto = {
                   status:      sigstatus,
                   # was the set of signatures described under encrypted cover?
                   encrypted:   bool,
+                  # which of the headers is covered by sigstatus?
+                  headers:     [header_name*]
                 },
     decrypted?: {
                   status: msgdecstatus,
+                  # map encrypted headers that differed from the outside headers.
+                  # the value of each item in the map is what that field showed externally
+                  # (maybe null if it was not present in the external headers).
+                  header-mask:  { header_name*: string|null }
                 }
 }
 
index b1f6a4bbb3567dbb3391fbfd34c099c274983729..4dfe9c1d9def25c6294e335f999d2b5891b33ebb 100644 (file)
@@ -645,6 +645,12 @@ format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
                        sp->map_key (sp, "encrypted");
                        sp->boolean (sp, msg_crypto->signature_encrypted);
                    }
+                   if (msg_crypto->payload_subject) {
+                       sp->map_key (sp, "headers");
+                       sp->begin_list (sp);
+                       sp->string (sp, "Subject");
+                       sp->end (sp);
+                   }
                    sp->end (sp);
                }
                if (msg_crypto->decryption_status != NOTMUCH_MESSAGE_DECRYPTED_NONE) {
@@ -652,6 +658,21 @@ format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
                    sp->begin_map (sp);
                    sp->map_key (sp, "status");
                    sp->string (sp, msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL ? "full" : "partial");
+
+                   if (msg_crypto->payload_subject) {
+                       const char *subject = g_mime_message_get_subject GMIME_MESSAGE (node->part);
+                       if (subject == NULL || strcmp (subject, msg_crypto->payload_subject)) {
+                           /* protected subject differs from the external header */
+                           sp->map_key (sp, "header-mask");
+                           sp->begin_map (sp);
+                           sp->map_key (sp, "Subject");
+                           if (subject == NULL)
+                               sp->null (sp);
+                           else
+                               sp->string (sp, subject);
+                           sp->end (sp);
+                       }
+                   }
                    sp->end (sp);
                }
            }
index 8a8fef6a674284ab03d2ad09e07530cf91bf11f9..68d431e95939056ac584006acec90f3a0aa96004 100755 (executable)
@@ -22,7 +22,7 @@ test_json_nodes <<<"$output" \
 test_begin_subtest "verify protected header is visible with decryption"
 output=$(notmuch show --decrypt=true --format=json id:protected-header@crypto.notmuchmail.org)
 test_json_nodes <<<"$output" \
-                'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full"}}' \
+                'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full", "header-mask": {"Subject": "Subject Unavailable"}}}' \
                 'subject:[0][0][0]["headers"]["Subject"]="This is a protected header"'
 
 test_begin_subtest "misplaced protected headers should not be made visible during decryption"
@@ -58,7 +58,7 @@ test_json_nodes <<<"$output" \
 test_begin_subtest "verify nested message/rfc822 protected header is visible"
 output=$(notmuch show --decrypt=true --format=json id:nested-rfc822-message@crypto.notmuchmail.org)
 test_json_nodes <<<"$output" \
-                'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full"}}' \
+                'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full", "header-mask": {"Subject": "Subject Unavailable"}}}' \
                 'subject:[0][0][0]["headers"]["Subject"]="This is a message using draft-melnikov-smime-header-signing"'
 
 test_done