X-Git-Url: https://vcs.fsf.org/?a=blobdiff_plain;f=edward;h=d7443927cabc24fa373c66b8019015be5d981dd2;hb=cbe017bc9637a97c6275a1e3992760ee70f3860e;hp=2480fcd513a7a26b744a04df4d90a9d53e80a56f;hpb=a32a1e11dc9742516809b46b49b1bbdc8adaaa12;p=edward.git diff --git a/edward b/edward index 2480fcd..d744392 100755 --- a/edward +++ b/edward @@ -26,7 +26,7 @@ Code sourced from these projects: - * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/PREVIOUS-20150530/edward.tgz + * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/PREVIOUS-20150530/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 """ @@ -37,8 +37,8 @@ import os import sys import enum import gpgme +import smtplib import importlib -import subprocess import email.parser import email.message @@ -51,7 +51,7 @@ from email.mime.nonmultipart import MIMENonMultipart import edward_config -langs = ["de", "el", "en", "es", "fr", "it", "ja", "pt-br", "ro", "ru", "tr"] +langs = ["de", "el", "en", "es", "fr", "it", "ja", "pt-br", "ro", "ru", "tr", "zh"] """This list contains the abbreviated names of reply languages available to edward.""" @@ -92,8 +92,9 @@ class EddyMsg (object): 'payload_pieces' is a list of objects containing strings that when strung together form the fully-decoded string representation of the mime part. - The 'filename', 'content_type' and 'description_list' come from the mime - part parameters. + The 'filename', 'content_type', 'content_disposition' and + 'description_list' come from the mime part parameters. + """ multipart = False @@ -104,6 +105,7 @@ class EddyMsg (object): filename = None content_type = None + content_disposition = None description_list = None @@ -265,13 +267,18 @@ def main (): print_reply_only = handle_args() + email_bytes = sys.stdin.buffer.read() + + test_auto_reply(email_bytes) + gpgme_ctx = get_gpg_context(edward_config.gnupghome, edward_config.sign_with_key) - email_bytes = sys.stdin.buffer.read() + # do this twice so sigs can be verified with newly imported keys + parse_pgp_mime(email_bytes, gpgme_ctx) email_struct = parse_pgp_mime(email_bytes, gpgme_ctx) - email_to, email_from, email_subject = email_to_from_subject(email_bytes) + email_to, email_reply_to, email_subject = email_to_reply_to_subject(email_bytes) lang, reply_from = import_lang_pick_address(email_to, edward_config.hostname) replyinfo_obj = ReplyInfo() @@ -281,14 +288,14 @@ def main (): get_key_from_fp(replyinfo_obj, gpgme_ctx) reply_plaintext = write_reply(replyinfo_obj) - reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \ + reply_mime = generate_encrypted_mime(reply_plaintext, email_reply_to, reply_from, \ email_subject, replyinfo_obj.encrypt_to_key, gpgme_ctx) if print_reply_only == True: print(reply_mime) else: - send_reply(reply_mime, email_subject, email_from, reply_from) + send_reply(reply_mime, email_reply_to, reply_from) def get_gpg_context (gnupghome, sign_with_key_fp): @@ -362,7 +369,7 @@ def parse_mime(msg_struct): each sub-part is also a EddyMsg instance. Args: - msg_struct: an email parsed with email.parser.Parser(), which can be + msg_struct: an email parsed with email.parser.BytesParser(), which can be multi-part Returns: @@ -404,8 +411,7 @@ def scan_and_split (payload_piece, match_name, pattern): return [payload_piece] flags = re.DOTALL | re.MULTILINE - matches = re.search("(?P.*?)(?P" + pattern + - ")(?P.*)", payload_piece.string, flags=flags) + matches = re.search(pattern, payload_piece.string, flags=flags) if matches == None: pieces = [payload_piece] @@ -413,15 +419,15 @@ def scan_and_split (payload_piece, match_name, pattern): else: beginning = PayloadPiece() - beginning.string = matches.group('beginning') + beginning.string = payload_piece.string[:matches.start()] beginning.piece_type = payload_piece.piece_type match = PayloadPiece() - match.string = matches.group('match') + match.string = payload_piece.string[matches.start():matches.end()] match.piece_type = match_name rest = PayloadPiece() - rest.string = matches.group('rest') + rest.string = payload_piece.string[matches.end():] rest.piece_type = payload_piece.piece_type more_pieces = scan_and_split(rest, match_name, pattern) @@ -433,11 +439,11 @@ def scan_and_split (payload_piece, match_name, pattern): def get_subpart_data (part): """This function grabs information from a mime part. - It copies needed data from a email.parser.Parser() object over to an + It copies needed data from an email.parser.BytesParser() object over to an EddyMsg object. Args: - part: a mime.parser.Parser() object + part: an email.parser.BytesParser() object Returns: an EddyMsg() object @@ -454,10 +460,13 @@ def get_subpart_data (part): payload_string = part.as_string() if payload_string != None: - obj.payload_bytes = payload_string.encode(charset) + # convert each isolated carriage return or line feed to carriage return + line feed + payload_string_crlf = re.sub(r'\n', '\r\n', re.sub(r'\r', '\n', re.sub(r'\r\n', '\n', payload_string))) + obj.payload_bytes = payload_string_crlf.encode(charset) obj.filename = part.get_filename() obj.content_type = part.get_content_type() + obj.content_disposition = part['content-disposition'] obj.description_list = part['content-description'] if mime_decoded_bytes != None: @@ -887,7 +896,9 @@ def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part): replyinfo_obj.target_key = piece.gpg_data.sigs[0] break else: - if piece.piece_type == TxtType.text: + if (eddymsg_obj.content_disposition == None \ + or not eddymsg_obj.content_disposition.startswith("attachment")) \ + and piece.piece_type == TxtType.text: replyinfo_obj.msg_to_quote += piece.string @@ -964,15 +975,6 @@ def write_reply (replyinfo_obj): if replyinfo_obj.decrypt_success == True: debug('decrypt success') reply_plain += replyinfo_obj.replies['success_decrypt'] - - if (replyinfo_obj.sig_success == True) and (replyinfo_obj.have_reply_key == True): - debug('message quoted') - reply_plain += replyinfo_obj.replies['space'] - reply_plain += replyinfo_obj.replies['quote_follows'] - reply_plain += "\n\n" - quoted_text = email_quote_text(replyinfo_obj.msg_to_quote) - reply_plain += quoted_text - reply_plain += "\n\n" elif replyinfo_obj.decrypt_failure == True: @@ -1009,6 +1011,17 @@ def write_reply (replyinfo_obj): reply_plain += "\n\n" + if (replyinfo_obj.decrypt_success == True) \ + and (replyinfo_obj.sig_success == True) \ + and (replyinfo_obj.have_reply_key == True): + debug('message quoted') + reply_plain += replyinfo_obj.replies['quote_follows'] + reply_plain += "\n\n" + quoted_text = email_quote_text(replyinfo_obj.msg_to_quote) + reply_plain += quoted_text + reply_plain += "\n\n" + + if (reply_plain == ""): debug('plaintext message') reply_plain += replyinfo_obj.replies['failed_decrypt'] @@ -1054,9 +1067,9 @@ def add_gpg_key (key_block, gpgme_ctx): try: key_obj = gpgme_ctx.get_key(fingerprint) except: - pass + key_obj = None - if is_key_usable(key_obj): + if key_obj != None and is_key_usable(key_obj): key_fingerprints += [fingerprint] key_cannot_encrypt = False @@ -1232,8 +1245,34 @@ def is_key_usable (key_obj): return False -def email_to_from_subject (email_bytes): - """Returns the values of the email's To:, From: and Subject: fields +def test_auto_reply (email_bytes): + """Test whether email is auto-generated + + If the email is autogenerated, edward quits without sending a response. + This is not a perfect test. Some auto-responses will go undetected. + + Args: + email_bytes: the byte string from of the email + + Returns: + Nothing, or exits the program + """ + + email_struct = email.parser.BytesHeaderParser().parsebytes(email_bytes) + + auto_submitted = email_struct['Auto-Submitted'] + + if auto_submitted == None or auto_submitted == "no" \ + or auto_submitted == "No": + + return + + debug("autoreply") + exit(0) + + +def email_to_reply_to_subject (email_bytes): + """Returns the email's To:, Reply-To: (or From:), and Subject: fields Returns this information from an email. @@ -1241,16 +1280,21 @@ def email_to_from_subject (email_bytes): email_bytes: the byte string form of the email Returns: - the email To:, From:, and Subject: fields as strings + the email To:, Reply-To: (or From:), and Subject: fields as strings """ - email_struct = email.parser.BytesParser().parsebytes(email_bytes) + email_struct = email.parser.BytesHeaderParser().parsebytes(email_bytes) email_to = email_struct['To'] email_from = email_struct['From'] + email_reply_to = email_struct['Reply-To'] + email_subject = email_struct['Subject'] - return email_to, email_from, email_subject + if email_reply_to == None: + email_reply_to = email_from + + return email_to, email_reply_to, email_subject def import_lang_pick_address(email_to, hostname): @@ -1290,8 +1334,8 @@ def import_lang_pick_address(email_to, hostname): return lang_module, reply_from -def generate_encrypted_mime (plaintext, email_to, email_subject, encrypt_to_key, - gpgme_ctx): +def generate_encrypted_mime (plaintext, email_to, email_from, email_subject, + encrypt_to_key, gpgme_ctx): """This function creates the mime email reply. It can encrypt the email. If the encrypt_key is included, then the email is encrypted and signed. @@ -1339,7 +1383,10 @@ def generate_encrypted_mime (plaintext, email_to, email_subject, encrypt_to_key, else: message_mime = plaintext_mime + message_mime['Auto-Submitted'] = 'auto-replied' + message_mime['To'] = email_to + message_mime['From'] = email_from message_mime['Subject'] = email_subject reply = message_mime.as_string() @@ -1347,18 +1394,27 @@ def generate_encrypted_mime (plaintext, email_to, email_subject, encrypt_to_key, return reply -def send_reply(email_txt, subject, reply_to, reply_from): +def send_reply(email_txt, reply_to, reply_from): + """Sends reply email + + Sent to original sender - email_bytes = email_txt.encode('ascii') + Args: + email_txt: message as a string + reply_to: recipient of reply + reply_from: edward's specific email address - p = subprocess.Popen(["/usr/sbin/sendmail", "-f", reply_from, "-F", "Edward, GPG Bot", "-i", reply_to], stdin=subprocess.PIPE) + Post: + Email is sent + """ - (stdout, stderr) = p.communicate(email_bytes) + if reply_to == None: + error("*** ERROR: No one to send email to.") + exit(1) - if stdout != None: - debug("sendmail stdout: " + str(stdout)) - if stderr != None: - error("sendmail stderr: " + str(stderr)) + s = smtplib.SMTP('localhost') + s.sendmail(reply_from, reply_to, email_txt) + s.quit() def email_quote_text (text):