created higher order function used by flatten_eddy
[edward.git] / edward
diff --git a/edward b/edward
index 8fc26132f2c43a4f8d2b9d0693c5665284e15b52..c7adf7138cb8b670d31273557664d3e8dd8f9f73 100755 (executable)
--- a/edward
+++ b/edward
@@ -29,7 +29,6 @@ Code sourced from these projects:
   * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/CURRENT/edward.tar.gz
   * https://git-tails.immerda.ch/whisperback/tree/whisperBack/encryption.py?h=feature/python3
   * http://www.physics.drexel.edu/~wking/code/python/send_pgp_mime
-
 """
 
 import sys
@@ -48,6 +47,37 @@ from email.mime.nonmultipart    import MIMENonMultipart
 
 import edward_config
 
+match_types =  [('encrypted',
+                '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
+                ('pubkey',
+                '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
+                ('detachedsig',
+                '-----END PGP SIGNATURE-----.*?-----BEGIN PGP SIGNATURE-----'),
+                ('clearsign',
+                '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
+
+
+class EddyMsg (object):
+    def __init__(self):
+        self.multipart          = False
+        self.subparts           = []
+
+        self.charset            = None
+        self.payload_bytes      = None
+        self.payload_pieces     = []
+
+        self.filename           = None
+        self.content_type       = None
+        self.description_list   = None
+
+
+class PayloadPiece (object):
+    def __init__(self):
+        self.piece_type         = None
+        self.string             = None
+        self.gpg_data           = None
+
+
 def main ():
 
     handle_args()
@@ -56,16 +86,18 @@ def main ():
                               edward_config.sign_with_key)
 
     email_text = sys.stdin.read()
-    email_from, email_subject = email_from_subject(email_text)
+    result = parse_pgp_mime(email_text)
 
-    plaintext, keys = email_decode_flatten(email_text, gpgme_ctx, False)
-    encrypt_to_key = choose_reply_encryption_key(keys)
+    email_from, email_subject = email_from_subject(email_text)
 
-    reply_message = generate_reply(plaintext, email_from, \
-                                   email_subject, encrypt_to_key,
-                                   gpgme_ctx)
+#    plaintext, fingerprints = email_decode_flatten(email_text, gpgme_ctx, False)
+#    encrypt_to_key = choose_reply_encryption_key(gpgme_ctx, fingerprints)
+#
+#    reply_message = generate_reply(plaintext, email_from, \
+#                                   email_subject, encrypt_to_key,
+#                                   gpgme_ctx)
 
-    print(reply_message)
+    print(flatten_eddy(result))
 
 
 def get_gpg_context (gnupghome, sign_with_key_fp):
@@ -87,91 +119,146 @@ def get_gpg_context (gnupghome, sign_with_key_fp):
     return gpgme_ctx
 
 
-def email_decode_flatten (email_text, gpgme_ctx, from_decryption):
-
-    body = ""
-    keys = []
+def parse_pgp_mime (email_text):
 
     email_struct = email.parser.Parser().parsestr(email_text)
 
-    for subpart in email_struct.walk():
+    eddy_obj = parse_mime(email_struct)
+    eddy_obj = split_payloads(eddy_obj)
 
-        payload, description, filename, content_type \
-                = get_email_subpart_info(subpart)
+    return eddy_obj
 
-        if payload == "":
-            continue
 
-        if content_type == "multipart":
-            continue
+def parse_mime(msg_struct):
 
-        if content_type == "application/pgp-encrypted":
-            if ((description == "PGP/MIME version identification")
-                and (payload.strip() != "Version: 1")):
-                    debug("Warning: unknown " + description
-                          + ": " + payload.strip())
-            # ignore the version number
-            continue
+    eddy_obj = EddyMsg()
 
-        if (filename == "encrypted.asc") or (content_type == "pgp/mime"):
-            plaintext, more_keys = decrypt_text(payload, gpgme_ctx)
+    if msg_struct.is_multipart() == True:
+        payloads = msg_struct.get_payload()
 
-            body += plaintext
-            keys += more_keys
+        eddy_obj.multipart = True
+        eddy_obj.subparts = list(map(parse_mime, payloads))
 
-        elif content_type == "application/pgp-keys":
-            keys += add_gpg_keys(payload, gpgme_ctx)
+    else:
+        eddy_obj = get_subpart_data(msg_struct)
 
-        elif content_type == "text/plain":
-            if from_decryption == True:
-                body += payload + "\n"
+    return eddy_obj
 
-                keys += add_gpg_keys(payload, gpgme_ctx)
 
-            else:
-                plaintext, more_keys = decrypt_text(payload, gpgme_ctx)
+def split_payloads (eddy_obj):
 
-                body += plaintext
-                keys += more_keys
+    if eddy_obj.multipart == True:
+        eddy_obj.subparts = list(map(split_payloads, eddy_obj.subparts))
 
-                keys += add_gpg_keys(payload, gpgme_ctx)
+    else:
+        for (match_type, pattern) in match_types:
 
-    return body, keys
+            new_pieces_list = []
+            for payload_piece in eddy_obj.payload_pieces:
+                new_pieces_list += scan_and_split(payload_piece,
+                    match_type, pattern)
+            eddy_obj.payload_pieces = new_pieces_list
 
+    return eddy_obj
 
-def email_from_subject (email_text):
 
-    email_struct = email.parser.Parser().parsestr(email_text)
+def scan_and_split (payload_piece, match_type, pattern):
 
-    email_from      = email_struct['From']
-    email_subject   = email_struct['Subject']
+    flags = re.DOTALL | re.MULTILINE
+    matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
+                        ")(?P<rest>.*)", payload_piece.string, flags=flags)
 
-    return email_from, email_subject
+    if matches == None:
+        pieces = [payload_piece]
 
+    else:
 
-def get_email_subpart_info (part):
+        beginning               = PayloadPiece()
+        beginning.string        = matches.group('beginning')
+        beginning.piece_type    = payload_piece.piece_type
 
-    charset             = part.get_content_charset()
-    payload_bytes       = part.get_payload(decode=True)
+        match                   = PayloadPiece()
+        match.string            = matches.group('match')
+        match.piece_type        = match_type
 
-    filename            = part.get_filename()
-    content_type        = part.get_content_type()
-    description_list    = part.get_params(header='content-description')
+        rest                    = PayloadPiece()
+        rest.string             = matches.group('rest')
+        rest.piece_type         = payload_piece.piece_type
 
-    if charset == None:
-        charset = 'utf-8'
+        more_pieces = scan_and_split(rest, match_type, pattern)
 
-    if payload_bytes != None:
-        payload = payload_bytes.decode(charset)
-    else:
-        payload = ""
+        if more_pieces == None:
+            pieces = [beginning, match, rest]
+        else:
+            pieces = [beginning, match] + more_pieces
+
+    return pieces
+
+
+def get_subpart_data (part):
+
+    obj = EddyMsg()
+
+    obj.charset             = part.get_content_charset()
+    obj.payload_bytes       = part.get_payload(decode=True)
+
+    obj.filename            = part.get_filename()
+    obj.content_type        = part.get_content_type()
+    obj.description_list    = part['content-description']
+
+    # your guess is as good as a-myy-ee-ine...
+    if obj.charset == None:
+        obj.charset = 'utf-8'
+
+    if obj.payload_bytes != None:
+        try:
+            payload = PayloadPiece()
+            payload.string = obj.payload_bytes.decode(obj.charset)
+            payload.piece_type = 'text'
 
-    if description_list != None:
-        description = description_list[0][0]
+            obj.payload_pieces = [payload]
+        except UnicodeDecodeError:
+            pass
+
+    return obj
+
+
+def do_to_eddys_pieces (function_to_do, eddy_obj, data):
+
+    if eddy_obj.multipart == True:
+        result_list = []
+        for sub in eddy_obj.subparts:
+            result_list += do_to_eddys_pieces(function_to_do, sub, data)
     else:
-        description = ""
+        result_list = [function_to_do(eddy_obj.payload_pieces, data)]
+
+    return result_list
+
+
+def flatten_eddy (eddy_obj):
+
+    string = "\n".join(do_to_eddys_pieces(flatten_payload_pieces, eddy_obj, None))
+
+    return string
 
-    return payload, description, filename, content_type
+
+def flatten_payload_pieces (payload_pieces, _ignore):
+
+    string = ""
+    for piece in payload_pieces:
+        string += piece.string
+
+    return string
+
+
+def email_from_subject (email_text):
+
+    email_struct = email.parser.Parser().parsestr(email_text)
+
+    email_from      = email_struct['From']
+    email_subject   = email_struct['Subject']
+
+    return email_from, email_subject
 
 
 def add_gpg_keys (text, gpgme_ctx):
@@ -180,7 +267,7 @@ def add_gpg_keys (text, gpgme_ctx):
                                '-----BEGIN PGP PUBLIC KEY BLOCK-----',
                                '-----END PGP PUBLIC KEY BLOCK-----')
 
-    keys = []
+    fingerprints = []
     for key_block in key_blocks:
         fp = io.BytesIO(key_block.encode('ascii'))
 
@@ -189,17 +276,17 @@ def add_gpg_keys (text, gpgme_ctx):
 
         if imports != []:
             fingerprint = imports[0][0]
-            keys += [gpgme_ctx.get_key(fingerprint)]
+            fingerprints += [fingerprint]
 
             debug("added gpg key: " + fingerprint)
 
-    return keys
+    return fingerprints
 
 
 def decrypt_text (gpg_text, gpgme_ctx):
 
     body = ""
-    keys = []
+    fingerprints = []
 
     msg_blocks = scan_and_grab(gpg_text,
                                '-----BEGIN PGP MESSAGE-----',
@@ -212,15 +299,38 @@ def decrypt_text (gpg_text, gpgme_ctx):
         sigs        = pair[1]
 
         for sig in sigs:
-            keys += [gpgme_ctx.get_key(sig.fpr)]
+            fingerprints += [sig.fpr]
 
         # recursive for nested layers of mime and/or gpg
-        plaintext, more_keys = email_decode_flatten(plaintext, gpgme_ctx, True)
+        plaintext, more_fps = email_decode_flatten(plaintext, gpgme_ctx, True)
 
         body += plaintext
-        keys += more_keys
+        fingerprints += more_fps
+
+    return body, fingerprints
+
+
+def verify_clear_signature (text, gpgme_ctx):
 
-    return body, keys
+    sig_blocks = scan_and_grab(text,
+                               '-----BEGIN PGP SIGNED MESSAGE-----',
+                               '-----END PGP SIGNATURE-----')
+
+    fingerprints = []
+    plaintext = ""
+
+    for sig_block in sig_blocks:
+        msg_fp = io.BytesIO(sig_block.encode('utf-8'))
+        ptxt_fp = io.BytesIO()
+
+        result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
+
+        plaintext += ptxt_fp.getvalue().decode('utf-8')
+        fingerprint = result[0].fpr
+
+        fingerprints += [fingerprint]
+
+    return plaintext, fingerprints
 
 
 def scan_and_grab (text, start_text, end_text):
@@ -255,13 +365,19 @@ def decrypt_block (msg_block, gpgme_ctx):
     return (plaintext, sigs)
 
 
-def choose_reply_encryption_key (keys):
+def choose_reply_encryption_key (gpgme_ctx, fingerprints):
 
     reply_key = None
-    for key in keys:
-        if (key.can_encrypt == True):
-            reply_key = key
-            break
+    for fp in fingerprints:
+        try:
+            key = gpgme_ctx.get_key(fp)
+
+            if (key.can_encrypt == True):
+                reply_key = key
+                break
+        except:
+            continue
+
 
     return reply_key