summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Whitton <spwhitton@spwhitton.name>2019-12-04 07:38:11 -0700
committerSean Whitton <spwhitton@spwhitton.name>2019-12-04 07:38:11 -0700
commit7fc83b718cdf6e7abddb61810d2cc6bd3e584689 (patch)
tree6a0d4eb57b60de897a6f80a24f0782949a294daf
parent7a5bdac3d0dc9d02ca75276bbb8fc1142247aaf1 (diff)
parentf3e1e5a552db990b2213ef850e056ecb7995558d (diff)
downloadmailscripts-7fc83b718cdf6e7abddb61810d2cc6bd3e584689.tar.gz
Merge tag 'debian/0.16-1' into buster-bpo
mailscripts release 0.16-1 for unstable (sid) [dgit] [dgit distro=debian no-split --quilt=linear] # gpg: Signature made Thu 28 Nov 2019 11:30:44 AM MST # gpg: using RSA key 9B917007AE030E36E4FC248B695B7AE4BF066240 # gpg: Good signature from "Sean Whitton <spwhitton@spwhitton.name>" [ultimate] # Primary key fingerprint: 8DC2 487E 51AB DD90 B5C4 753F 0F56 D055 3B6D 411B # Subkey fingerprint: 9B91 7007 AE03 0E36 E4FC 248B 695B 7AE4 BF06 6240
-rw-r--r--Makefile6
-rw-r--r--debian/changelog24
-rw-r--r--debian/control12
-rw-r--r--debian/elpa-test1
-rwxr-xr-xemail-print-mime-structure75
-rw-r--r--email-print-mime-structure.1.pod25
-rw-r--r--mailscripts.el2
-rwxr-xr-xtests/email-print-mime-structure.sh40
-rw-r--r--tests/email-print-mime-structure/alternative.eml18
-rw-r--r--tests/email-print-mime-structure/alternative.out3
-rw-r--r--tests/email-print-mime-structure/attachment.eml26
-rw-r--r--tests/email-print-mime-structure/attachment.out5
-rw-r--r--tests/email-print-mime-structure/bob.key82
-rw-r--r--tests/email-print-mime-structure/bob.p1275
-rw-r--r--tests/email-print-mime-structure/encrypted.eml37
-rw-r--r--tests/email-print-mime-structure/encrypted.out9
l---------tests/email-print-mime-structure/encrypted.pgpkey1
-rw-r--r--tests/email-print-mime-structure/simple.eml9
-rw-r--r--tests/email-print-mime-structure/simple.out1
-rw-r--r--tests/email-print-mime-structure/smime-encrypted.eml24
-rw-r--r--tests/email-print-mime-structure/smime-encrypted.out7
l---------tests/email-print-mime-structure/smime-encrypted.p121
-rw-r--r--tests/email-print-mime-structure/smime-signed.eml41
-rw-r--r--tests/email-print-mime-structure/smime-signed.out7
24 files changed, 504 insertions, 27 deletions
diff --git a/Makefile b/Makefile
index cd8f592..af30616 100644
--- a/Makefile
+++ b/Makefile
@@ -7,9 +7,13 @@ COMPLETIONS=completions/bash/email-print-mime-structure
all: $(MANPAGES) $(COMPLETIONS)
+check:
+ ./tests/email-print-mime-structure.sh
+ mypy --strict ./email-print-mime-structure
+
clean:
rm -f $(MANPAGES)
- rm -rf completions
+ rm -rf completions .mypy_cache
%.1: %.1.pod
pod2man --section=1 --date="Debian Project" --center="User Commands" \
diff --git a/debian/changelog b/debian/changelog
index 25929cc..3d6c274 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,27 @@
+mailscripts (0.16-1) unstable; urgency=medium
+
+ email-print-mime-structure:
+ * Add a test suite (Closes: #945451).
+ - Add <!nocheck> builddeps on
+ + python3
+ + python3-pgpy
+ + gpg
+ + gpg-agent
+ + mypy.
+ * Add capability to decrypt S/MIME-encrypted messages (Closes: #945195).
+ - Add gpgsm, openssl to Build-Depends and Suggests.
+ - Add <!nocheck> build-dep on dirmngr.
+ * Add capability to handle one-part PKCS#7 signature objects.
+ - Add gnutls-bin to Build-Depends and Suggests.
+ * Fully decode encrypted parts before passing to any decryption
+ mechanism.
+ See <https://bugs.debian.org/945195#100>.
+
+ Thanks to Daniel Kahn Gillmor for two patch series implementing these
+ improvements.
+
+ -- Sean Whitton <spwhitton@spwhitton.name> Thu, 28 Nov 2019 11:27:38 -0700
+
mailscripts (0.15-1~bpo10+1) buster-backports; urgency=medium
* Rebuild for buster-backports.
diff --git a/debian/control b/debian/control
index 72b57c3..3de5b11 100644
--- a/debian/control
+++ b/debian/control
@@ -7,8 +7,17 @@ Build-Depends:
bash-completion,
debhelper (>= 10),
dh-elpa,
+ dirmngr <!nocheck>,
+ gnutls-bin <!nocheck>,
+ gpg <!nocheck>,
+ gpg-agent <!nocheck>,
+ gpgsm <!nocheck>,
+ mypy <!nocheck>,
+ openssl <!nocheck>,
perl,
+ python3 <!nocheck>,
python3-argcomplete,
+ python3-pgpy <!nocheck>,
Vcs-Git: https://git.spwhitton.name/mailscripts
Vcs-Browser: https://git.spwhitton.name/mailscripts
@@ -44,8 +53,11 @@ Recommends:
python3-argcomplete,
python3-pgpy,
Suggests:
+ gnutls-bin,
gpg,
gpg-agent,
+ gpgsm,
+ openssl,
Architecture: all
Description: collection of scripts for manipulating e-mail on Debian
This package provides a collection of scripts for manipulating e-mail
diff --git a/debian/elpa-test b/debian/elpa-test
new file mode 100644
index 0000000..4d6556c
--- /dev/null
+++ b/debian/elpa-test
@@ -0,0 +1 @@
+disable = true
diff --git a/email-print-mime-structure b/email-print-mime-structure
index 27fb532..b7646e0 100755
--- a/email-print-mime-structure
+++ b/email-print-mime-structure
@@ -32,6 +32,7 @@ something like "cat -n"
'''
import os
import sys
+import enum
import email
import logging
import subprocess
@@ -51,6 +52,8 @@ try:
except ImportError:
argcomplete = None
+EncType = enum.Enum('EncType', ['PGPMIME', 'SMIME'])
+
class MimePrinter(object):
def __init__(self, args:Namespace):
self.args = args
@@ -79,32 +82,66 @@ class MimePrinter(object):
print(f'{prefix}{z.get_content_type()}{cset}{disposition}{fname} {nbytes:d} bytes')
cryptopayload:Optional[Message] = None
- ciphertext:Union[List[Message],str,bytes,None] = None
try_pgp_decrypt:bool = self.args.pgpkey or self.args.use_gpg_agent
+ try_cms_decrypt:bool = self.args.cmskey or self.args.use_gpg_agent
if try_pgp_decrypt and \
(parent is not None) and \
(parent.get_content_type().lower() == 'multipart/encrypted') and \
(str(parent.get_param('protocol')).lower() == 'application/pgp-encrypted') and \
(num == 2):
- ciphertext = z.get_payload()
- if not isinstance(ciphertext, str):
- logging.warning('encrypted part was not a leaf mime part somehow')
- return
- if self.args.pgpkey:
- cryptopayload = self.pgpy_decrypt(self.args.pgpkey, ciphertext)
- if cryptopayload is None and self.args.use_gpg_agent:
- cryptopayload = self.gpg_decrypt(ciphertext)
- if cryptopayload is None:
- logging.warning(f'Unable to decrypt')
- return
+ cryptopayload = self.decrypt_part(z, EncType.PGPMIME)
+
+ if try_cms_decrypt and \
+ cryptopayload is None and \
+ z.get_content_type().lower() == 'application/pkcs7-mime' and \
+ str(z.get_param('smime-type')).lower() in ['authenveloped-data',
+ 'enveloped-data']:
+ cryptopayload = self.decrypt_part(z, EncType.SMIME)
if cryptopayload is not None:
newprefix = prefix[:-3] + ' '
print(f'{newprefix}↧ (decrypts to)')
self.print_tree(cryptopayload, newprefix + '└', z, 0)
-
- def pgpy_decrypt(self, keys:List[str], ciphertext:str) -> Optional[Message]:
+ else:
+ if z.get_content_type().lower() == 'application/pkcs7-mime' and \
+ str(z.get_param('smime-type')).lower() == 'signed-data':
+ bodypart:Union[List[Message],str,bytes,None] = z.get_payload(decode=True)
+ if isinstance(bodypart, bytes):
+ unwrapped = self.pipe_transform(bodypart, ['certtool', '--p7-show-data', '--p7-info', '--inder'])
+ if unwrapped:
+ newprefix = prefix[:-3] + ' '
+ print(f'{newprefix}⇩ (unwraps to)')
+ self.print_tree(unwrapped, newprefix + '└', z, 0)
+ else:
+ logging.warning(f'Unable to unwrap one-part PKCS#7 signed message (maybe try "apt install gnutls-bin")')
+
+
+ def decrypt_part(self, msg:Message, flavor:EncType) -> Optional[Message]:
+ ciphertext:Union[List[Message],str,bytes,None] = msg.get_payload(decode=True)
+ cryptopayload:Optional[Message] = None
+ if not isinstance(ciphertext, bytes):
+ logging.warning('encrypted part was not a leaf mime part somehow')
+ return None
+ if flavor == EncType.PGPMIME:
+ if self.args.pgpkey:
+ cryptopayload = self.pgpy_decrypt(self.args.pgpkey, ciphertext)
+ if cryptopayload is None and self.args.use_gpg_agent:
+ cryptopayload = self.pipe_transform(ciphertext, ['gpg', '--batch', '--decrypt'])
+ elif flavor == EncType.SMIME:
+ if self.args.cmskey:
+ for keyname in self.args.cmskey:
+ cmd = ['openssl', 'smime', '-decrypt', '-inform', 'DER', '-inkey', keyname]
+ cryptopayload = self.pipe_transform(ciphertext, cmd)
+ if cryptopayload:
+ return cryptopayload
+ if self.args.use_gpg_agent:
+ cryptopayload = self.pipe_transform(ciphertext, ['gpgsm', '--batch', '--decrypt'])
+ if cryptopayload is None:
+ logging.warning(f'Unable to decrypt')
+ return cryptopayload
+
+ def pgpy_decrypt(self, keys:List[str], ciphertext:bytes) -> Optional[Message]:
if pgpy is None:
logging.warning(f'Python module pgpy is not available, not decrypting (try "apt install python3-pgpy")')
return None
@@ -121,13 +158,13 @@ class MimePrinter(object):
pass
return None
- def gpg_decrypt(self, ciphertext:str) -> Optional[Message]:
+ def pipe_transform(self, ciphertext:bytes, cmd:List[str]) -> Optional[Message]:
inp:int
outp:int
inp, outp = os.pipe()
- with open(outp, 'w') as outf:
+ with open(outp, 'wb') as outf:
outf.write(ciphertext)
- out:subprocess.CompletedProcess[bytes] = subprocess.run(['gpg', '--batch', '--decrypt'],
+ out:subprocess.CompletedProcess[bytes] = subprocess.run(cmd,
stdin=inp,
capture_output=True)
if out.returncode == 0:
@@ -157,7 +194,9 @@ def main() -> None:
parser:ArgumentParser = ArgumentParser(description='Read RFC2822 MIME message from stdin and emit a tree diagram to stdout.',
epilog="Example: email-print-mime-structure <message.eml")
parser.add_argument('--pgpkey', metavar='KEYFILE', action='append',
- help='OpenPGP Transferable Secret Key for decrypting')
+ help='OpenPGP Transferable Secret Key for decrypting PGP/MIME')
+ parser.add_argument('--cmskey', metavar='KEYFILE', action='append',
+ help='X.509 Private Key for decrypting S/MIME')
parser.add_argument('--use-gpg-agent', action='store_true',
help='Ask local GnuPG installation for decryption')
parser.add_argument('--no-use-gpg-agent', action='store_false',
diff --git a/email-print-mime-structure.1.pod b/email-print-mime-structure.1.pod
index d8545ad..037c1a9 100644
--- a/email-print-mime-structure.1.pod
+++ b/email-print-mime-structure.1.pod
@@ -32,15 +32,26 @@ key.
OpenPGP secret keys listed in B<--pgpkey=> are used ephemerally, and
do not interact with any local GnuPG keyring.
+=item B<--cmskey=>I<KEYFILE>
+
+I<KEYFILE> should name a PEM- or DER-encoded X.509 private key that is
+not password-protected. If an S/MIME-encrypted message that uses CMS
+is found on standard input, this key will be tried for decryption.
+May be used multiple times if you want to try decrypting with more
+than one such key.
+
+X.509 private keys listed in B<--cmskey=> are used ephemerally, and do
+not interact with any local GnuPG keyring.
+
=item B<--use-gpg-agent>
If this flag is present, and B<email-print-mime-structure> encounters
-a PGP/MIME-encrypted part, it will try to decrypt the part using the
-secret keys found in the local installation of GnuPG.
+a PGP/MIME- or S/MIME-encrypted part, it will try to decrypt the part
+using the secret keys found in the local installation of GnuPG.
-If both B<--pgpkey=>I<KEYFILE> and B<--use-gpg-agent> are
-supplied, I<KEYFILE> arguments will be tried before falling back to
-GnuPG.
+If B<--use-gpg-agent> is supplied along with either
+B<--pgpkey=>I<KEYFILE> or B<--cmskey=>I<KEYFILE> arguments, the
+I<KEYFILE> arguments will be tried before falling back to GnuPG.
If B<email-print-mime-structure> has been asked to decrypt parts with
either B<--pgpkey=>I<KEYFILE> or with B<--use-gpg-agent>, and it
@@ -49,8 +60,8 @@ stderr.
=item B<--no-use-gpg-agent>
-Don't try to decrypt PGP/MIME-encrypted parts using secret keys found
-in the local installation of GnuPG. This is the default.
+Don't try to decrypt PGP/MIME- or S/MIME-encrypted parts using secret
+keys found in the local installation of GnuPG. This is the default.
=item B<--help>, B<-h>
diff --git a/mailscripts.el b/mailscripts.el
index 9c51f73..6872dd6 100644
--- a/mailscripts.el
+++ b/mailscripts.el
@@ -1,7 +1,7 @@
;;; mailscripts.el --- functions to access tools in the mailscripts package
;; Author: Sean Whitton <spwhitton@spwhitton.name>
-;; Version: 0.15
+;; Version: 0.16
;; Package-Requires: (notmuch projectile)
;; Copyright (C) 2018, 2019 Sean Whitton
diff --git a/tests/email-print-mime-structure.sh b/tests/email-print-mime-structure.sh
new file mode 100755
index 0000000..b45294e
--- /dev/null
+++ b/tests/email-print-mime-structure.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+set -e
+
+test_eml() {
+ message="$1"
+ shift
+ diff -u "$message.out" <(./email-print-mime-structure "$@" <"$message.eml")
+}
+
+for eml in tests/email-print-mime-structure/*.eml; do
+ base="${eml%%.eml}"
+ pgpkey="$base.pgpkey"
+ p12key="$base.p12"
+ if [ -e "$pgpkey" ]; then
+ printf "Testing %s (PGPy)\n" "${eml##*/}"
+ test_eml "$base" --pgpkey "$pgpkey"
+
+ testgpghome=$(mktemp -d)
+ printf "Testing %s (GnuPG PGP/MIME)\n" "${eml##*/}"
+ gpg --homedir="$testgpghome" --batch --quiet --import <"$pgpkey"
+ GNUPGHOME="$testgpghome" test_eml "$base" --use-gpg-agent
+ rm -rf "$testgpghome"
+ elif [ -e "$p12key" ]; then
+ printf "Testing %s (OpenSSL)\n" "${eml##*/}"
+ grep -v ^- < "$p12key" | base64 -d | \
+ openssl pkcs12 -nocerts -nodes -passin pass: -passout pass: -out "$base.pemkey"
+ test_eml "$base" --cmskey "$base.pemkey"
+ rm -f "$base.pemkey"
+
+ testgpghome=$(mktemp -d)
+ printf "Testing %s (GnuPG S/MIME)\n" "${eml##*/}"
+ gpgsm --pinentry-mode=loopback --passphrase-fd 4 4<<<'' --homedir="$testgpghome" --batch --quiet --import <"$p12key"
+ GNUPGHOME="$testgpghome" test_eml "$base" --use-gpg-agent
+ rm -rf "$testgpghome"
+ else
+ printf "Testing %s\n" "${eml##*/}"
+ test_eml "$base"
+ fi
+done
diff --git a/tests/email-print-mime-structure/alternative.eml b/tests/email-print-mime-structure/alternative.eml
new file mode 100644
index 0000000..9b7723d
--- /dev/null
+++ b/tests/email-print-mime-structure/alternative.eml
@@ -0,0 +1,18 @@
+Date: Sun, 24 Nov 2019 21:13:45 -0500
+Subject: test message
+Message-ID: <alternative@mailscripts.example>
+From: Alice <alice@mailscripts.example>
+To: Bob <bob@mailscripts.example>
+Content-Type: multipart/alternative; boundary="abc123"
+
+--abc123
+Content-Type: text/plain
+
+This is a simple message
+
+--abc123
+Content-Type: text/html
+
+<html><head></head><body><p>This is a simple message</p></body></html>
+
+--abc123--
diff --git a/tests/email-print-mime-structure/alternative.out b/tests/email-print-mime-structure/alternative.out
new file mode 100644
index 0000000..1c0165a
--- /dev/null
+++ b/tests/email-print-mime-structure/alternative.out
@@ -0,0 +1,3 @@
+└┬╴multipart/alternative 414 bytes
+ ├─╴text/plain 26 bytes
+ └─╴text/html 72 bytes
diff --git a/tests/email-print-mime-structure/attachment.eml b/tests/email-print-mime-structure/attachment.eml
new file mode 100644
index 0000000..31baa7c
--- /dev/null
+++ b/tests/email-print-mime-structure/attachment.eml
@@ -0,0 +1,26 @@
+Date: Sun, 24 Nov 2019 21:13:45 -0500
+Subject: test message
+Message-ID: <attachment@mailscripts.example>
+From: Alice <alice@mailscripts.example>
+To: Bob <bob@mailscripts.example>
+Content-Type: multipart/mixed; boundary="xyz"
+
+--xyz
+Content-Type: multipart/alternative; boundary="abc123"
+
+--abc123
+Content-Type: text/plain
+
+This is a simple message
+
+--abc123
+Content-Type: text/html
+
+<html><head></head><body><p>This is a simple message</p></body></html>
+
+--abc123--
+--xyz
+Content-Type: text/plain
+Content-Disposition: attachment; filename="test.txt"
+
+This is a simple attachment file.
diff --git a/tests/email-print-mime-structure/attachment.out b/tests/email-print-mime-structure/attachment.out
new file mode 100644
index 0000000..f2ca543
--- /dev/null
+++ b/tests/email-print-mime-structure/attachment.out
@@ -0,0 +1,5 @@
+└┬╴multipart/mixed 594 bytes
+ ├┬╴multipart/alternative 234 bytes
+ │├─╴text/plain 26 bytes
+ │└─╴text/html 72 bytes
+ └─╴text/plain attachment [test.txt] 33 bytes
diff --git a/tests/email-print-mime-structure/bob.key b/tests/email-print-mime-structure/bob.key
new file mode 100644
index 0000000..59e981b
--- /dev/null
+++ b/tests/email-print-mime-structure/bob.key
@@ -0,0 +1,82 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Comment: Bob's OpenPGP Transferable Secret Key
+
+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
+=miES
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/tests/email-print-mime-structure/bob.p12 b/tests/email-print-mime-structure/bob.p12
new file mode 100644
index 0000000..9fab0f7
--- /dev/null
+++ b/tests/email-print-mime-structure/bob.p12
@@ -0,0 +1,75 @@
+-----BEGIN PKCS12-----
+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
+CNOpwA/Myb75AgIoAA==
+-----END PKCS12-----
diff --git a/tests/email-print-mime-structure/encrypted.eml b/tests/email-print-mime-structure/encrypted.eml
new file mode 100644
index 0000000..5a28719
--- /dev/null
+++ b/tests/email-print-mime-structure/encrypted.eml
@@ -0,0 +1,37 @@
+Date: Sun, 24 Nov 2019 21:13:45 -0500
+Subject: test message
+Message-ID: <pgpmime-encrypted@mailscripts.example>
+From: Alice <alice@mailscripts.example>
+To: Bob <bob@mailscripts.example>
+Content-Type: multipart/encrypted; boundary="eeee";
+ protocol="application/pgp-encrypted"
+
+--eeee
+content-type: application/pgp-encrypted
+
+Version: 1
+
+--eeee
+content-type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+hQGMA3wvqk35PDeyAQv/YL3qVwxaAzTrk7/YysmH+HZs8A5DS7WarPpQ6Y33LZ/1
+KFmVujDxbAg4xBDyDJYGmvlKXnbA/rWCAWI+tc67EoSO35hAFK0h6hUhHCCdAy+g
+p9coNsZS+XF9xw8Bfd4Bfr+iaHojqVsBJJXGVg/ie3PWMwC4vvllLsk42Uibw5Oa
+AiEvvm/UTOL/4hEiH2G3Au68e6ulC8q/PNPp4vOD7xMUTg319f+ZkR9pLs2KZles
+o+gioquDvdZhroGmPem9pUsLyjQ83TwYtvaPGoA5uLCuIL1jkroIFPjp96/JqzDR
+RmsEQ3GzWVwxcWyjpw/6gU5SfPrfgaTLLvm6MgYIbAFYEG1cTMbkl3W2vR0VUecr
+uL9YDqmBHgS5XhkxE5tvs0vo8pyIXg96Td1tJIu1IOA87XXVD8Uo6ypTffggOIpO
+YWM0yRXXEOXTd+RwAbY4+Tpu4pE1dQbKF660iuFcoY3b9Bh8O4UG5eARI2gEiFb/
+UmmWp0GpUi4mbhq381rS0sBOAU6KB6t5lk8+IBjGQG8PDf8kovKZrL/zaSrfppqm
+d7ecohTZ41uYP+R6sYu6/LE3UfmTYWqavb0SLwzB55oXhEQBj/NeI/7kwPKrAvfC
+64hxR1Zkd5JfJO6Qwmp7+Z7PB6f1yvBuNhupZqAxP9Dp7F/+b5RaCdSaVIrNUarP
+xRbgaCvL/RDp3HQ1SQlEMNrkIdALXw6xSYvCYxCFnrJOAqeAUtdf3JLVS7jIyuWC
+SeWJwd17o7wkzp7uz6IdK+f+wcNTxPogvpM/rjBmAFE8PHZkgjZlZLJFMDFx4ZiI
+Ljn7AwKRH225pKV3ixwTTFlNCtjO6BgtaSEIRLXw6a95iAsdO140qGRCIRAYMxww
+=9ORE
+-----END PGP MESSAGE-----
+
+--eeee--
+
diff --git a/tests/email-print-mime-structure/encrypted.out b/tests/email-print-mime-structure/encrypted.out
new file mode 100644
index 0000000..a2db072
--- /dev/null
+++ b/tests/email-print-mime-structure/encrypted.out
@@ -0,0 +1,9 @@
+└┬╴multipart/encrypted 1366 bytes
+ ├─╴application/pgp-encrypted 11 bytes
+ └─╴application/octet-stream 971 bytes
+ ↧ (decrypts to)
+ └┬╴multipart/mixed 415 bytes
+ ├┬╴multipart/alternative 234 bytes
+ │├─╴text/plain 26 bytes
+ │└─╴text/html 72 bytes
+ └─╴text/plain attachment [test.txt] 33 bytes
diff --git a/tests/email-print-mime-structure/encrypted.pgpkey b/tests/email-print-mime-structure/encrypted.pgpkey
new file mode 120000
index 0000000..9ba5070
--- /dev/null
+++ b/tests/email-print-mime-structure/encrypted.pgpkey
@@ -0,0 +1 @@
+bob.key \ No newline at end of file
diff --git a/tests/email-print-mime-structure/simple.eml b/tests/email-print-mime-structure/simple.eml
new file mode 100644
index 0000000..36facec
--- /dev/null
+++ b/tests/email-print-mime-structure/simple.eml
@@ -0,0 +1,9 @@
+Date: Sun, 24 Nov 2019 21:13:45 -0500
+Subject: test message
+Message-ID: <simple@mailscripts.example>
+From: Alice <alice@mailscripts.example>
+To: Bob <bob@mailscripts.example>
+
+This is a simple message
+
+
diff --git a/tests/email-print-mime-structure/simple.out b/tests/email-print-mime-structure/simple.out
new file mode 100644
index 0000000..7ac3514
--- /dev/null
+++ b/tests/email-print-mime-structure/simple.out
@@ -0,0 +1 @@
+└─╴text/plain 30 bytes
diff --git a/tests/email-print-mime-structure/smime-encrypted.eml b/tests/email-print-mime-structure/smime-encrypted.eml
new file mode 100644
index 0000000..d24bcb3
--- /dev/null
+++ b/tests/email-print-mime-structure/smime-encrypted.eml
@@ -0,0 +1,24 @@
+Date: Sun, 24 Nov 2019 21:13:45 -0500
+Subject: test message
+Message-ID: <smime-encrypted@mailscripts.example>
+From: Alice <alice@mailscripts.example>
+To: Bob <bob@mailscripts.example>
+Content-Type: application/pkcs7-mime; smime-type="enveloped-data"
+Content-Transfer-Encoding: base64
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggFhMIIBXQIBADBFMC0xKzApBgNVBAMTIlNhbXBsZSBMQU1Q
+UyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkCFCJT7jBtAgsfAs31ycE+Ot95phvCMA0GCSqGSIb3DQEB
+AQUABIIBAHBxGgCzLx/IlX44zQ8YZuSrkGcZw/X3GKh6zVKAxMPG2O0t/5zLHIaO8sDdDi0L+wQI
+F5erkeWFXaicNIQ279xytVyavscLRMl594et15Z4zaRPCwtUOg3QmHrl1fnsgiEj/PHVAxTHBF+m
+FYCSIs5kwRylLZeDwJOkIcGPYiXiyBz65FwxcUVUMXeTnbQ6wJOvCAbXWZzkgceIa4j51kqDp4ed
+ab+cxohJnAR/E5tgVkYTKGEkkc9kQdKvuv3ZWOfbV7bkWuTy6tIu7XGrAhed7zWnIvwL0kkK2Jt+
+d288j+aXDAi/lh94OTc35FhqS3/SsnZ6b1RnIUxGHfjSXAwwgAYJKoZIhvcNAQcBMB0GCWCGSAFl
+AwQBAgQQjRLFAT2Ajt5aLQBK1SmPmKCABIIBoCOFGqO1Pda3XKu29ntdGH20bXN2FkWBBxjFBeT/
+t/1gU94GthUCJWeAlIrNRcl7F+Nx5NukmpfEx7Pu1fX057H0j/IVYHGcbjZPnHXdliXqt4Y8KiSw
+FTxc42+Ma5YmagOQ4iuxTWnL/sSIDnw4aM3dJLyz8CuzBNox1tNIjFmNs6Bv/dFU/zubik1wjnH3
+XYE7a1ZWVsChCdsEO7g0MG6gxaJ9aKSeG1SMtQ+navxhJg8U6D2fcU/xN0uOhF3eDEmDtnRiTi7h
+3yXeOnDR9EYYGKvYG3xqUJPnP9iD15hsThuCIStilqWWFlq4WPKMMNlz+EiCSko1Xy+9EIAEHNxe
+/YwtgP2AQlJcDbVKay3d47EPeI0PFS3CaHTbEkiHyvubXBQRefY38me/gl+v0+Znw2ZRJ3v8Cs3Q
+FkmUga9b2tL0VUUk9RUFCJbexitwFICgWdJRhWiJvT4yxruTbokG1zDEIyaz8IObfbEp84STmdOW
+myVKq0nzbreWsP72l7wgD/GsXQWRMhlj3T1LStSxustGE7Ps4RGgDbYZJvLtBBBECnF/0MBqRk20
+pt/FtWhgAAAAAAAAAAAAAA==
diff --git a/tests/email-print-mime-structure/smime-encrypted.out b/tests/email-print-mime-structure/smime-encrypted.out
new file mode 100644
index 0000000..071be70
--- /dev/null
+++ b/tests/email-print-mime-structure/smime-encrypted.out
@@ -0,0 +1,7 @@
+└─╴application/pkcs7-mime 1196 bytes
+ ↧ (decrypts to)
+ └┬╴multipart/mixed 415 bytes
+ ├┬╴multipart/alternative 234 bytes
+ │├─╴text/plain 26 bytes
+ │└─╴text/html 72 bytes
+ └─╴text/plain attachment [test.txt] 33 bytes
diff --git a/tests/email-print-mime-structure/smime-encrypted.p12 b/tests/email-print-mime-structure/smime-encrypted.p12
new file mode 120000
index 0000000..ecb0ede
--- /dev/null
+++ b/tests/email-print-mime-structure/smime-encrypted.p12
@@ -0,0 +1 @@
+bob.p12 \ No newline at end of file
diff --git a/tests/email-print-mime-structure/smime-signed.eml b/tests/email-print-mime-structure/smime-signed.eml
new file mode 100644
index 0000000..3929d6b
--- /dev/null
+++ b/tests/email-print-mime-structure/smime-signed.eml
@@ -0,0 +1,41 @@
+Date: Sun, 24 Nov 2019 21:13:45 -0500
+Subject: test message
+Message-ID: <smime-signed@mailscripts.example>
+From: Alice <alice@mailscripts.example>
+To: Bob <bob@mailscripts.example>
+Content-Type: application/pkcs7-mime; smime-type="signed-data"
+Content-Transfer-Encoding: base64
+
+MIIHOgYJKoZIhvcNAQcCoIIHKzCCBycCAQExDTALBglghkgBZQMEAgEwggG+BgkqhkiG9w0BBwGg
+ggGvBIIBq0NvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOyBib3VuZGFyeT0ieHl6Ig0KDQot
+LXh5eg0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSJhYmMx
+MjMiDQoNCi0tYWJjMTIzDQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHNp
+bXBsZSBtZXNzYWdlDQoNCi0tYWJjMTIzDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbA0KDQo8aHRt
+bD48aGVhZD48L2hlYWQ+PGJvZHk+PHA+VGhpcyBpcyBhIHNpbXBsZSBtZXNzYWdlPC9wPjwvYm9k
+eT48L2h0bWw+DQoNCi0tYWJjMTIzLS0NCi0teHl6DQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW4N
+CkNvbnRlbnQtRGlzcG9zaXRpb246IGF0dGFjaG1lbnQ7IGZpbGVuYW1lPSJ0ZXN0LnR4dCINCg0K
+VGhpcyBpcyBhIHNpbXBsZSBhdHRhY2htZW50IGZpbGUuDQqgggNyMIIDbjCCAlagAwIBAgIUZ4K0
+WXNSS8H0cUcZavD9EYqqTAswDQYJKoZIhvcNAQENBQAwLTErMCkGA1UEAxMiU2FtcGxlIExBTVBT
+IENlcnRpZmljYXRlIEF1dGhvcml0eTAgFw0xOTExMjAwNjU0MThaGA8yMDUyMDkyNzA2NTQxOFow
+GTEXMBUGA1UEAxMOQWxpY2UgTG92ZWxhY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQDD7q35ZdG2JAzzJGNZDZ9sV7AKh0hlRfoFjTZN5m4RegQAYSyag43ouWi1xRN0avf0UTYrwjK0
+4qRdV7GzCACoEKq/xiNUOsjfJXzbCublN3fZMOXDshKKBqThlK75SjA9Czxg7ejGoiY/iidk0e91
+neK30SCCaBTJlfR2ZDrPk73IPMeksxoTatfF9hw9dDA+/Hi1yptN/aG0Q/s9icFrxr6y2zQXsjuQ
+PmjMZgj10aD9cazWVgRYCgflhmA0V1uQl1wobYU8DAVxVn+GgabqyjGQMoythIK0Gn5+ofwxXXUM
+/zbU+g6+1ISdoXxRRFtq2GzbIqkAHZZQm+BbnFrhAgMBAAGjgZcwgZQwDAYDVR0TAQH/BAIwADAe
+BgNVHREEFzAVgRNhbGljZUBzbWltZS5leGFtcGxlMBMGA1UdJQQMMAoGCCsGAQUFBwMEMA8GA1Ud
+DwEB/wQFAwMHoAAwHQYDVR0OBBYEFKwuVFqk/VUYry7oZkQ40SXR1wB5MB8GA1UdIwQYMBaAFLdS
+TXPAiD2yw3paDPOU9/eAonfbMA0GCSqGSIb3DQEBDQUAA4IBAQB76o4Yz7yrVSFcpXqLrcGtdI4q
+93aKCXECCCzNQLp4yesh6brqaZHNJtwYcJ5TqbUym9hJ70iJE4jGNN+yAZR1ltte0HFKYIBKM4EJ
+umG++2hqbUaLz4tl06BHaQPCv/9NiNY7q9R9c/B6s1YzHhwqkWht2a+AtgJ4BkpG+g+MmZMQV/Ao
+7RwLFKJ9OlMWLBmEXFcpIJN0HpPasT0nEl/MmotSu+8RnClAi3yFfyTKb+8rD7VxuyXetqDZ6dU/
+9/iqD/SZS7OQIjywtd343mACz3B1RlFxMHSA6dQAf2btGumqR0KiAp3KkYRAePoaJqYkB7Zad06n
+gFl0G0FHON+7MYIB2TCCAdUCAQEwRTAtMSswKQYDVQQDEyJTYW1wbGUgTEFNUFMgQ2VydGlmaWNh
+dGUgQXV0aG9yaXR5AhRngrRZc1JLwfRxRxlq8P0RiqpMCzALBglghkgBZQMEAgGgaTAYBgkqhkiG
+9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xOTExMjUyMDQ0MDZaMC8GCSqGSIb3
+DQEJBDEiBCCFhxQumT0jWdiOeuN21xClT1cnZAuhs2YCQjJWBu5fDjANBgkqhkiG9w0BAQEFAASC
+AQCau7ahdSrX99SsKrAVpTgL9ifxt5DgVywQQGOaKjsEHCRq8SH0BMuU+ByJryzkmsNFSUOo/Okg
+GZ5ZUlw+3W9Lnb9HM9Ad9WYZVT0LY7cqXDTV1ElQG7fnP6ugg/mOIw6UfqMc2SznGZX9ZGlOAwvk
+G7v/20xsw99ZQddcr4zSJ7V+bKxBmeysaMlGRKfwrEmu+/bDKGXRLstjsJmzvOK+LYqNKCFPhehf
+EjOJQyQq5sEHYc63gnziWTEcwoCQJcFGVZhh/x+KEdXtTb/jXpQzoX/IAs3ED/ggOzEyPbuMTrQD
+c9LA8e8NbwhGhKvNi0sZYccDsFMQLpo3rZttA4mo
diff --git a/tests/email-print-mime-structure/smime-signed.out b/tests/email-print-mime-structure/smime-signed.out
new file mode 100644
index 0000000..293a271
--- /dev/null
+++ b/tests/email-print-mime-structure/smime-signed.out
@@ -0,0 +1,7 @@
+└─╴application/pkcs7-mime 2538 bytes
+ ⇩ (unwraps to)
+ └┬╴multipart/mixed 415 bytes
+ ├┬╴multipart/alternative 234 bytes
+ │├─╴text/plain 26 bytes
+ │└─╴text/html 72 bytes
+ └─╴text/plain attachment [test.txt] 33 bytes