X-Git-Url: https://vcs.fsf.org/?a=blobdiff_plain;f=edward;h=d7443927cabc24fa373c66b8019015be5d981dd2;hb=cbe017bc9637a97c6275a1e3992760ee70f3860e;hp=1f46c009748d816184ad8db414acc389a81ea5b9;hpb=59bc3fac3c5e21a2fb79a2cb4ad97a7d6da480b6;p=edward.git diff --git a/edward b/edward index 1f46c00..d744392 100755 --- a/edward +++ b/edward @@ -26,7 +26,7 @@ Code sourced from these projects: - * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/CURRENT/edward.tar.gz + * 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 @@ -150,8 +152,7 @@ class GPGData (object): verify the signature on a block of text. 'key_cannot_encrypt' is set to True if pubkeys or sigs' keys in the payload - piece are not capable of encryption. This could happen if a key is revoked - or expired, for instance. + piece are not capable of encryption, are revoked or expired, for instance. 'keys' is a list of fingerprints of keys obtained in public key blocks. """ @@ -266,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() @@ -282,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): @@ -363,14 +369,14 @@ 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: an instance of EddyMsg, potentially a recursive one. """ - eddymsg_obj = EddyMsg() + eddymsg_obj = get_subpart_data(msg_struct) if msg_struct.is_multipart() == True: payloads = msg_struct.get_payload() @@ -378,9 +384,6 @@ def parse_mime(msg_struct): eddymsg_obj.multipart = True eddymsg_obj.subparts = list(map(parse_mime, payloads)) - else: - eddymsg_obj = get_subpart_data(msg_struct) - return eddymsg_obj @@ -408,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] @@ -417,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) @@ -435,16 +437,16 @@ def scan_and_split (payload_piece, match_name, pattern): def get_subpart_data (part): - """This function grabs information from a single part mime object. + """This function grabs information from a mime part. - It copies needed data from a single part email.parser.Parser() object over - to an EddyMsg object. + It copies needed data from an email.parser.BytesParser() object over to an + EddyMsg object. Args: - part: a non-multi-part mime.parser.Parser() object + part: an email.parser.BytesParser() object Returns: - a single-part EddyMsg() object + an EddyMsg() object """ obj = EddyMsg() @@ -458,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: @@ -891,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 @@ -929,7 +936,7 @@ def get_key_from_fp (replyinfo_obj, gpgme_ctx): except gpgme.GpgmeError: continue - if encrypt_to_key.can_encrypt == True: + if is_key_usable(encrypt_to_key): replyinfo_obj.encrypt_to_key = encrypt_to_key replyinfo_obj.have_reply_key = True replyinfo_obj.key_can_encrypt = True @@ -968,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: @@ -1013,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'] @@ -1058,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 key_obj.can_encrypt == True: + if key_obj != None and is_key_usable(key_obj): key_fingerprints += [fingerprint] key_cannot_encrypt = False @@ -1203,7 +1212,7 @@ def get_signature_fp (sigs, gpgme_ctx): sigkey_missing = True continue - if key_obj.can_encrypt == True: + if is_key_usable(key_obj): fingerprints += [sig.fpr] key_cannot_encrypt = False sigkey_missing = False @@ -1218,8 +1227,52 @@ def get_signature_fp (sigs, gpgme_ctx): return (fingerprints, sigkey_missing, key_cannot_encrypt) -def email_to_from_subject (email_bytes): - """Returns the values of the email's To:, From: and Subject: fields +def is_key_usable (key_obj): + """Returns boolean representing key usability regarding encryption + + Tests various feature of key and returns usability + + Args: + key_obj: a gpgme key object + + Returns: + A boolean representing key usability + """ + if key_obj.can_encrypt and not key_obj.invalid and not key_obj.expired \ + and not key_obj.revoked and not key_obj.disabled: + return True + else: + return False + + +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. @@ -1227,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): @@ -1276,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. @@ -1325,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() @@ -1333,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 - email_bytes = email_txt.encode('ascii') + Sent to original sender - p = subprocess.Popen(["/usr/sbin/sendmail", "-f", reply_from, "-F", "Edward, GPG Bot", "-i", reply_to], stdin=subprocess.PIPE) + Args: + email_txt: message as a string + reply_to: recipient of reply + reply_from: edward's specific email address - (stdout, stderr) = p.communicate(email_bytes) + Post: + Email is sent + """ + + 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):