import re
import io
import os
+import importlib
import email.parser
import email.message
import edward_config
+langs = ["an", "de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
+
match_types = [('clearsign',
'-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----'),
('message',
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 ():
edward_config.sign_with_key)
email_text = sys.stdin.read()
- result = parse_pgp_mime(email_text, gpgme_ctx)
+ email_struct = parse_pgp_mime(email_text, gpgme_ctx)
+
+ email_to, email_from, email_subject = email_to_from_subject(email_text)
+ lang = import_lang(email_to)
- email_from, email_subject = email_from_subject(email_text)
+ replyinfo_obj = ReplyInfo()
+ replyinfo_obj.replies = lang.replies
-# 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)
+ prepare_for_reply(email_struct, replyinfo_obj)
+ encrypt_to_key = get_key_from_fp(replyinfo_obj, gpgme_ctx)
+ reply_plaintext = write_reply(replyinfo_obj)
- print(flatten_eddy(result))
+ # 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)
+
+ print(reply_mime)
def get_gpg_context (gnupghome, sign_with_key_fp):
email_struct = email.parser.Parser().parsestr(email_text)
- eddy_obj = parse_mime(email_struct)
- eddy_obj = split_payloads(eddy_obj)
- eddy_obj = decrypt_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)
-
- return eddy_obj
-
+ eddymsg_obj = get_subpart_data(msg_struct)
-def split_payloads (eddy_obj):
-
- if eddy_obj.multipart == True:
- eddy_obj.subparts = list(map(split_payloads, eddy_obj.subparts))
-
- else:
- for (match_type, pattern) in match_types:
-
- 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
+ return eddymsg_obj
def scan_and_split (payload_piece, match_type, pattern):
+ # don't try to re-split pieces containing gpg data
+ if payload_piece.piece_type != "text":
+ return [payload_piece]
+
flags = re.DOTALL | re.MULTILINE
matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
")(?P<rest>.*)", payload_piece.string, flags=flags)
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.payload_pieces, data)]
+ function_to_do(eddymsg_obj, data)
+
- return result_list
+def split_payloads (eddymsg_obj):
+ for match_type in match_types:
+ do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_type)
-def decrypt_payloads (eddy_obj, gpgme_ctx):
- do_to_eddys_pieces(decrypt_payload_pieces, eddy_obj, gpgme_ctx)
+def split_payload_pieces (eddymsg_obj, match_type):
- return eddy_obj
+ (match_name, pattern) = match_type
+ new_pieces_list = []
+ for piece in eddymsg_obj.payload_pieces:
+ new_pieces_list += scan_and_split(piece, match_name, pattern)
-def decrypt_payload_pieces (payload_pieces, gpgme_ctx):
+ eddymsg_obj.payload_pieces = new_pieces_list
- for piece in payload_pieces:
+
+def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
+
+ if eddymsg_obj.multipart == True:
+ prev_parts=[]
+ for sub in eddymsg_obj.subparts:
+ gpg_on_payloads (sub, gpgme_ctx, prev_parts)
+ prev_parts += [sub]
+
+ return
+
+ 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":
+ key_fps = add_gpg_key(piece.string, gpgme_ctx)
+
+ if key_fps != []:
+ piece.gpg_data = GPGData()
+ piece.gpg_data.keys = key_fps
+
+ elif piece.piece_type == "clearsign":
+ (plaintext, sig_fps) = verify_clear_signature(piece.string, gpgme_ctx)
+
+ if sig_fps != []:
+ piece.gpg_data = GPGData()
+ 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
+ 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
+
else:
pass
-def flatten_eddy (eddy_obj):
+def prepare_for_reply (eddymsg_obj, replyinfo_obj):
+
+ do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj)
+
+def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
+
+ 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
- string = "\n".join(do_to_eddys_pieces(flatten_payload_pieces, eddy_obj, None))
+ if replyinfo_obj.target_key == None:
+ replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
- return string
-def flatten_payload_pieces (payload_pieces, _ignore):
+def flatten_payloads (eddymsg_obj):
- string = ""
- for piece in payload_pieces:
+ 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
+ flat_string += piece.string
elif piece.piece_type == "message":
- # recursive!
- string += flatten_eddy(piece.gpg_data.plainobj)
+ 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 string
+ return flat_string
-def email_from_subject (email_text):
- email_struct = email.parser.Parser().parsestr(email_text)
+def get_key_from_fp (replyinfo_obj, gpgme_ctx):
- email_from = email_struct['From']
- email_subject = email_struct['Subject']
+ if replyinfo_obj.target_key == None:
+ replyinfo_obj.target_key = replyinfo_obj.fallback_target_key
- return email_from, email_subject
+ if replyinfo_obj.target_key != None:
+ try:
+ encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key)
+ return encrypt_to_key
+ except:
+ pass
-def add_gpg_keys (text, gpgme_ctx):
+ # no available key to use
+ replyinfo_obj.target_key = None
+ replyinfo_obj.fallback_target_key = None
- key_blocks = scan_and_grab(text,
- '-----BEGIN PGP PUBLIC KEY BLOCK-----',
- '-----END PGP PUBLIC KEY BLOCK-----')
+ replyinfo_obj.no_public_key = True
+ replyinfo_obj.public_key_received = False
- fingerprints = []
- for key_block in key_blocks:
- fp = io.BytesIO(key_block.encode('ascii'))
+ return None
- result = gpgme_ctx.import_(fp)
- imports = result.imports
- if imports != []:
- fingerprint = imports[0][0]
- fingerprints += [fingerprint]
+def write_reply (replyinfo_obj):
- debug("added gpg key: " + fingerprint)
+ reply_plain = ""
- return fingerprints
+ 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
-def decrypt_text (gpg_text, gpgme_ctx):
+ elif replyinfo_obj.failed_decrypt == True:
+ reply_plain += replyinfo_obj.replies['failed_decrypt']
- body = ""
- fingerprints = []
- msg_blocks = scan_and_grab(gpg_text,
- '-----BEGIN PGP MESSAGE-----',
- '-----END PGP MESSAGE-----')
+ if replyinfo_obj.sig_success == True:
+ reply_plain += "\n\n"
+ reply_plain += replyinfo_obj.replies['sig_success']
- plaintexts_and_sigs = decrypt_blocks(msg_blocks, gpgme_ctx)
+ elif replyinfo_obj.sig_failure == True:
+ reply_plain += "\n\n"
+ reply_plain += replyinfo_obj.replies['sig_failure']
- for pair in plaintexts_and_sigs:
- plaintext = pair[0]
- sigs = pair[1]
- for sig in sigs:
- fingerprints += [sig.fpr]
+ if replyinfo_obj.public_key_received == True:
+ reply_plain += "\n\n"
+ reply_plain += replyinfo_obj.replies['public_key_received']
- # recursive for nested layers of mime and/or gpg
- plaintext, more_fps = email_decode_flatten(plaintext, gpgme_ctx, True)
+ elif replyinfo_obj.no_public_key == True:
+ reply_plain += "\n\n"
+ reply_plain += replyinfo_obj.replies['no_public_key']
- body += plaintext
- fingerprints += more_fps
- return body, fingerprints
+ reply_plain += "\n\n"
+ reply_plain += replyinfo_obj.replies['signature']
+ return reply_plain
-def verify_clear_signature (text, gpgme_ctx):
- sig_blocks = scan_and_grab(text,
- '-----BEGIN PGP SIGNED MESSAGE-----',
- '-----END PGP SIGNATURE-----')
+def add_gpg_key (key_block, gpgme_ctx):
- fingerprints = []
- plaintext = ""
+ fp = io.BytesIO(key_block.encode('ascii'))
- for sig_block in sig_blocks:
- msg_fp = io.BytesIO(sig_block.encode('utf-8'))
- ptxt_fp = io.BytesIO()
+ result = gpgme_ctx.import_(fp)
+ imports = result.imports
- result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
+ key_fingerprints = []
- plaintext += ptxt_fp.getvalue().decode('utf-8')
- fingerprint = result[0].fpr
+ if imports != []:
+ for import_ in imports:
+ fingerprint = import_[0]
+ key_fingerprints += [fingerprint]
- fingerprints += [fingerprint]
+ debug("added gpg key: " + fingerprint)
- return plaintext, fingerprints
+ return key_fingerprints
-def scan_and_grab (text, start_text, end_text):
+def verify_clear_signature (sig_block, gpgme_ctx):
- matches = re.search('(' + start_text + '.*' + end_text + ')',
- text, flags=re.DOTALL)
+ # FIXME: this might require the un-decoded bytes
+ # or the correct re-encoding with the carset of the mime part.
+ msg_fp = io.BytesIO(sig_block.encode('utf-8'))
+ ptxt_fp = io.BytesIO()
- if matches != None:
- match_tuple = matches.groups()
- else:
- match_tuple = ()
+ result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
+
+ # FIXME: this might require using the charset of the mime part.
+ plaintext = ptxt_fp.getvalue().decode('utf-8')
+
+ sig_fingerprints = []
+ for res_ in result:
+ sig_fingerprints += [res_.fpr]
+
+ return plaintext, sig_fingerprints
+
+
+def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
- return match_tuple
+ detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
+ plaintext_fp = io.BytesIO(plaintext_bytes)
+ ptxt_fp = io.BytesIO()
+ result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
-def decrypt_blocks (msg_blocks, gpgme_ctx):
+ sig_fingerprints = []
+ for res_ in result:
+ sig_fingerprints += [res_.fpr]
- return [decrypt_block(block, gpgme_ctx) for block in msg_blocks]
+ return sig_fingerprints
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):
return reply_key
-def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
- gpgme_ctx):
+def email_to_from_subject (email_text):
+
+ email_struct = email.parser.Parser().parsestr(email_text)
+ email_to = email_struct['To']
+ email_from = email_struct['From']
+ email_subject = email_struct['Subject']
- reply = "To: " + email_from + "\n"
- reply += "Subject: " + email_subject + "\n"
+ return email_to, email_from, email_subject
- 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
+def import_lang(email_to):
+
+ if email_to != None:
+ for lang in langs:
+ if "edward-" + lang in email_to:
+ lang = "lang." + re.sub('-', '_', lang)
+ language = importlib.import_module(lang)
+
+ return language
- # 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)
+ return importlib.import_module("lang.en")
+
+
+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
+
+ # 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):
encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
encrypt_to_key,
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
def error (error_msg):
- sys.stderr.write(progname + ": " + error_msg + "\n")
+ sys.stderr.write(progname + ": " + str(error_msg) + "\n")
def debug (debug_msg):