generate the encrypted mime reply
[edward.git] / edward
diff --git a/edward b/edward
index ccfdce1a1e3e16483f32adea8271d7d499b45fb7..21c26a13ab2b43b8725c1807d1b934827c712e63 100755 (executable)
--- a/edward
+++ b/edward
@@ -89,6 +89,21 @@ class GPGData (object):
         self.sigs               = []
         self.keys               = []
 
+class ReplyInfo (object):
+    def __init__(self):
+        self.replies                = None
+
+        self.target_key             = None
+        self.fallback_target_key    = None
+        self.msg_to_quote           = ""
+
+        self.success_decrypt        = False
+        self.failed_decrypt         = False
+        self.public_key_received    = False
+        self.no_public_key          = False
+        self.sig_success            = False
+        self.sig_failure            = False
+
 
 def main ():
 
@@ -103,16 +118,19 @@ def main ():
     email_to, email_from, email_subject = email_to_from_subject(email_text)
     lang = import_lang(email_to)
 
-    reply_plaintext = build_reply(email_struct)
+    replyinfo_obj = ReplyInfo()
+    replyinfo_obj.replies = lang.replies
+
+    prepare_for_reply(email_struct, replyinfo_obj)
+    encrypt_to_key = get_key_from_fp(replyinfo_obj, gpgme_ctx)
+    reply_plaintext = write_reply(replyinfo_obj)
 
-    debug(lang.replies['success_decrypt'])
-    print(reply_plaintext)
+    # TODO error handling for missing keys and setting .no_public_key
+    reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \
+                                         email_subject, encrypt_to_key,
+                                         gpgme_ctx)
 
-#    encrypt_to_key = choose_reply_encryption_key(gpgme_ctx, fingerprints)
-#
-#    reply_mime = generate_encrypted_mime(plaintext, email_from, \
-#                                         email_subject, encrypt_to_key,
-#                                         gpgme_ctx)
+    print(reply_mime)
 
 
 def get_gpg_context (gnupghome, sign_with_key_fp):
@@ -138,27 +156,27 @@ def parse_pgp_mime (email_text, gpgme_ctx):
 
     email_struct = email.parser.Parser().parsestr(email_text)
 
-    eddy_obj = parse_mime(email_struct)
-    split_payloads(eddy_obj)
-    gpg_on_payloads(eddy_obj, gpgme_ctx)
+    eddymsg_obj = parse_mime(email_struct)
+    split_payloads(eddymsg_obj)
+    gpg_on_payloads(eddymsg_obj, gpgme_ctx)
 
-    return eddy_obj
+    return eddymsg_obj
 
 
 def parse_mime(msg_struct):
 
-    eddy_obj = EddyMsg()
+    eddymsg_obj = EddyMsg()
 
     if msg_struct.is_multipart() == True:
         payloads = msg_struct.get_payload()
 
-        eddy_obj.multipart = True
-        eddy_obj.subparts = list(map(parse_mime, payloads))
+        eddymsg_obj.multipart = True
+        eddymsg_obj.subparts = list(map(parse_mime, payloads))
 
     else:
-        eddy_obj = get_subpart_data(msg_struct)
+        eddymsg_obj = get_subpart_data(msg_struct)
 
-    return eddy_obj
+    return eddymsg_obj
 
 
 def scan_and_split (payload_piece, match_type, pattern):
@@ -222,119 +240,220 @@ def get_subpart_data (part):
     return obj
 
 
-def do_to_eddys_pieces (function_to_do, eddy_obj, data):
+def do_to_eddys_pieces (function_to_do, eddymsg_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)
+    if eddymsg_obj.multipart == True:
+        for sub in eddymsg_obj.subparts:
+            do_to_eddys_pieces(function_to_do, sub, data)
     else:
-        result_list = [function_to_do(eddy_obj, data)]
+        function_to_do(eddymsg_obj, data)
 
-    return result_list
 
-
-def split_payloads (eddy_obj):
+def split_payloads (eddymsg_obj):
 
     for match_type in match_types:
-        do_to_eddys_pieces(split_payload_pieces, eddy_obj, match_type)
+        do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_type)
 
 
-def split_payload_pieces (eddy_obj, match_type):
+def split_payload_pieces (eddymsg_obj, match_type):
 
     (match_name, pattern) = match_type
 
     new_pieces_list = []
-    for piece in eddy_obj.payload_pieces:
+    for piece in eddymsg_obj.payload_pieces:
         new_pieces_list += scan_and_split(piece, match_name, pattern)
 
-    eddy_obj.payload_pieces = new_pieces_list
+    eddymsg_obj.payload_pieces = new_pieces_list
 
 
-def gpg_on_payloads (eddy_obj, gpgme_ctx, prev_parts=[]):
+def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
 
-    if eddy_obj.multipart == True:
+    if eddymsg_obj.multipart == True:
         prev_parts=[]
-        for sub in eddy_obj.subparts:
+        for sub in eddymsg_obj.subparts:
             gpg_on_payloads (sub, gpgme_ctx, prev_parts)
             prev_parts += [sub]
 
+        return
 
-    for piece in eddy_obj.payload_pieces:
+    for piece in eddymsg_obj.payload_pieces:
 
         if piece.piece_type == "text":
             # don't transform the plaintext.
             pass
 
         elif piece.piece_type == "message":
-            (plaintext, sigs) = decrypt_block (piece.string, gpgme_ctx)
+            (plaintext, sigs) = decrypt_block(piece.string, gpgme_ctx)
 
             if plaintext:
                 piece.gpg_data = GPGData()
+                piece.gpg_data.decrypted = True
                 piece.gpg_data.sigs = sigs
                 # recurse!
                 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
 
         elif piece.piece_type == "pubkey":
-            fingerprints = add_gpg_key(piece.string, gpgme_ctx)
+            key_fps = add_gpg_key(piece.string, gpgme_ctx)
 
-            if fingerprints != []:
+            if key_fps != []:
                 piece.gpg_data = GPGData()
-                piece.gpg_data.keys = fingerprints
+                piece.gpg_data.keys = key_fps
 
         elif piece.piece_type == "clearsign":
-            (plaintext, fingerprints) = verify_clear_signature(piece.string, gpgme_ctx)
+            (plaintext, sig_fps) = verify_clear_signature(piece.string, gpgme_ctx)
 
-            if fingerprints != []:
+            if sig_fps != []:
                 piece.gpg_data = GPGData()
-                piece.gpg_data.sigs = fingerprints
+                piece.gpg_data.sigs = sig_fps
                 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
 
         elif piece.piece_type == "detachedsig":
             for prev in prev_parts:
                 payload_bytes = prev.payload_bytes
-            sigs_fps = verify_detached_signature(piece.string, payload_bytes, gpgme_ctx)
+                sig_fps = verify_detached_signature(piece.string, payload_bytes, gpgme_ctx)
+
+                if sig_fps != []:
+                    piece.gpg_data = GPGData()
+                    piece.gpg_data.sigs = sig_fps
+                    piece.gpg_data.plainobj = prev
+                    break
 
-            if sigs_fps != []:
-                piece.gpg_data = GPGData()
-                piece.gpg_data.sigs = sigs_fps
-                piece.gpg_data.plainobj = prev
         else:
             pass
 
 
-def build_reply (eddy_obj):
+def prepare_for_reply (eddymsg_obj, replyinfo_obj):
+
+    do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj)
 
-    string = "\n".join(do_to_eddys_pieces(build_reply_pieces, eddy_obj, None))
+def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
 
-    return string
+    for piece in eddymsg_obj.payload_pieces:
+        if piece.piece_type == "text":
+            # don't quote the plaintext part.
+            pass
+
+        elif piece.piece_type == "message":
+            if piece.gpg_data == None:
+                replyinfo_obj.failed_decrypt = True
+            else:
+                replyinfo_obj.success_decrypt = True
+
+                if replyinfo_obj.target_key != None:
+                    continue
+                if piece.gpg_data.sigs != []:
+                    replyinfo_obj.target_key = piece.gpg_data.sigs[0]
+                    replyinfo_obj.msg_to_quote = flatten_payloads(piece.gpg_data.plainobj)
+
+                # to catch public keys in encrypted blocks
+                prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
+
+        elif piece.piece_type == "pubkey":
+            if piece.gpg_data == None:
+                replyinfo_obj.no_public_key = True
+            else:
+                replyinfo_obj.public_key_received = True
 
+        elif (piece.piece_type == "clearsign") \
+                or (piece.piece_type == "detachedsig"):
+            if piece.gpg_data == None:
+                replyinfo_obj.sig_failure = True
+            else:
+                replyinfo_obj.sig_success = True
 
-def build_reply_pieces (eddy_obj, _ignore):
+                if replyinfo_obj.target_key == None:
+                    replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
 
-    string = ""
-    for piece in eddy_obj.payload_pieces:
+
+
+def flatten_payloads (eddymsg_obj):
+
+    flat_string = ""
+
+    if eddymsg_obj == None:
+        return ""
+
+    if eddymsg_obj.multipart == True:
+        for sub in eddymsg_obj.subparts:
+            flat_string += flatten_payloads (sub)
+
+        return flat_string
+
+    # todo: don't include nested decrypted messages.
+    for piece in eddymsg_obj.payload_pieces:
         if piece.piece_type == "text":
-            string += piece.string
-        elif piece.gpg_data == None:
-            string += "Hmmm... I wasn't able to get that part.\n"
+            flat_string += piece.string
         elif piece.piece_type == "message":
-            # recursive!
-            string += build_reply(piece.gpg_data.plainobj)
-        elif piece.piece_type == "pubkey":
-            string += "thanks for your public key:"
-            for key in piece.gpg_data.keys:
-                string += "\n" + key
-        elif piece.piece_type == "clearsign":
-            string += "*** Begin signed part ***\n"
-            string += build_reply(piece.gpg_data.plainobj)
-            string += "\n*** End signed part ***"
-        elif piece.piece_type == "detachedsig":
-            string += "*** Begin detached signed part ***\n"
-            string += build_reply(piece.gpg_data.plainobj)
-            string += "*** End detached signed part ***\n"
+            flat_string += flatten_payloads(piece.plainobj)
+        elif ((piece.piece_type == "clearsign") \
+                or (piece.piece_type == "detachedsig")) \
+                and (piece.gpg_data != None):
+                    flat_string += flatten_payloads (piece.gpg_data.plainobj)
+
+
+    return flat_string
 
-    return string
+
+def get_key_from_fp (replyinfo_obj, gpgme_ctx):
+
+    if replyinfo_obj.target_key == None:
+        replyinfo_obj.target_key = replyinfo_obj.fallback_target_key
+
+    if replyinfo_obj.target_key != None:
+        try:
+            encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key)
+            return encrypt_to_key
+
+        except:
+            pass
+
+    # no available key to use
+    replyinfo_obj.target_key = None
+    replyinfo_obj.fallback_target_key = None
+
+    replyinfo_obj.no_public_key = True
+    replyinfo_obj.public_key_received = False
+
+    return None
+
+
+def write_reply (replyinfo_obj):
+
+    reply_plain = ""
+
+    if replyinfo_obj.success_decrypt == True:
+        reply_plain += replyinfo_obj.replies['success_decrypt']
+
+        if replyinfo_obj.no_public_key == False:
+            quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
+            reply_plain += quoted_text
+
+    elif replyinfo_obj.failed_decrypt == True:
+        reply_plain += replyinfo_obj.replies['failed_decrypt']
+
+
+    if replyinfo_obj.sig_success == True:
+        reply_plain += "\n\n"
+        reply_plain += replyinfo_obj.replies['sig_success']
+
+    elif replyinfo_obj.sig_failure == True:
+        reply_plain += "\n\n"
+        reply_plain += replyinfo_obj.replies['sig_failure']
+
+
+    if replyinfo_obj.public_key_received == True:
+        reply_plain += "\n\n"
+        reply_plain += replyinfo_obj.replies['public_key_received']
+
+    elif replyinfo_obj.no_public_key == True:
+        reply_plain += "\n\n"
+        reply_plain += replyinfo_obj.replies['no_public_key']
+
+
+    reply_plain += "\n\n"
+    reply_plain += replyinfo_obj.replies['signature']
+
+    return reply_plain
 
 
 def add_gpg_key (key_block, gpgme_ctx):
@@ -344,16 +463,16 @@ def add_gpg_key (key_block, gpgme_ctx):
     result = gpgme_ctx.import_(fp)
     imports = result.imports
 
-    fingerprints = []
+    key_fingerprints = []
 
     if imports != []:
         for import_ in imports:
             fingerprint = import_[0]
-            fingerprints += [fingerprint]
+            key_fingerprints += [fingerprint]
 
             debug("added gpg key: " + fingerprint)
 
-    return fingerprints
+    return key_fingerprints
 
 
 def verify_clear_signature (sig_block, gpgme_ctx):
@@ -368,11 +487,11 @@ def verify_clear_signature (sig_block, gpgme_ctx):
     # FIXME: this might require using the charset of the mime part.
     plaintext = ptxt_fp.getvalue().decode('utf-8')
 
-    fingerprints = []
+    sig_fingerprints = []
     for res_ in result:
-        fingerprints += [res_.fpr]
+        sig_fingerprints += [res_.fpr]
 
-    return plaintext, fingerprints
+    return plaintext, sig_fingerprints
 
 
 def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
@@ -401,7 +520,11 @@ def decrypt_block (msg_block, gpgme_ctx):
         return ("",[])
 
     plaintext = plain_b.getvalue().decode('utf-8')
-    return (plaintext, sigs)
+
+    fingerprints = []
+    for sig in sigs:
+        fingerprints += [sig.fpr]
+    return (plaintext, fingerprints)
 
 
 def choose_reply_encryption_key (gpgme_ctx, fingerprints):
@@ -448,23 +571,17 @@ def import_lang(email_to):
 def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
                     gpgme_ctx):
 
+    # quoted printable encoding lets most ascii characters look normal
+    # before the decrypted mime message is decoded.
+    char_set = email.charset.Charset("utf-8")
+    char_set.body_encoding = email.charset.QP
 
-    reply  = "To: " + email_from + "\n"
-    reply += "Subject: " + email_subject + "\n"
+    # MIMEText doesn't allow setting the text encoding
+    # so we use MIMENonMultipart.
+    plaintext_mime = MIMENonMultipart('text', 'plain')
+    plaintext_mime.set_payload(plaintext, charset=char_set)
 
     if (encrypt_to_key != None):
-        plaintext_reply  = "thanks for the message!\n\n\n"
-        plaintext_reply += email_quote_text(plaintext)
-
-        # quoted printable encoding lets most ascii characters look normal
-        # before the decrypted mime message is decoded.
-        char_set = email.charset.Charset("utf-8")
-        char_set.body_encoding = email.charset.QP
-
-        # MIMEText doesn't allow setting the text encoding
-        # so we use MIMENonMultipart.
-        plaintext_mime = MIMENonMultipart('text', 'plain')
-        plaintext_mime.set_payload(plaintext_reply, charset=char_set)
 
         encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
                                               encrypt_to_key,
@@ -488,12 +605,13 @@ def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_ke
         message_mime.attach(encoded_mime)
         message_mime['Content-Disposition'] = 'inline'
 
-        reply += message_mime.as_string()
-
     else:
-        reply += "\n"
-        reply += "Sorry, i couldn't find your key.\n"
-        reply += "I'll need that to encrypt a message to you."
+        message_mime = plaintext_mime
+
+    message_mime['To'] = email_from
+    message_mime['Subject'] = email_subject
+
+    reply = message_mime.as_string()
 
     return reply