X-Git-Url: https://vcs.fsf.org/?a=blobdiff_plain;f=edward;h=d7443927cabc24fa373c66b8019015be5d981dd2;hb=cbe017bc9637a97c6275a1e3992760ee70f3860e;hp=aaed2a5c6ab8d99cec0d66aa1f0425d6c343d989;hpb=cfcc211c2dd148d6b2e92ae37784b73b124bab74;p=edward.git diff --git a/edward b/edward index aaed2a5..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 @@ -149,6 +151,9 @@ class GPGData (object): 'sigkey_missing' is set to True if edward doesn't have the key it needs to 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, are revoked or expired, for instance. + 'keys' is a list of fingerprints of keys obtained in public key blocks. """ @@ -157,6 +162,7 @@ class GPGData (object): plainobj = None sigs = [] sigkey_missing = False + key_cannot_encrypt = False keys = [] @@ -192,6 +198,9 @@ class ReplyInfo (object): 'sig_success' is set to True if edward could to some extent verify the signature of a signed part of the message to edward. + 'key_can_encrypt' is set to True if a key which can be encrypted to has + been found. + 'sig_failure' is set to True if edward could not verify a siganture. 'pubkey_success' is set to True if edward successfully imported a public @@ -200,6 +209,9 @@ class ReplyInfo (object): 'sigkey_missing' is set to True if edward doesn't have the public key needed for signature verification. + 'key_cannot_encrypt' is set to True if pubkeys or sig's keys in a payload + piece of the message are not capable of encryption. + 'have_repy_key' is set to True if edward has a public key to encrypt its reply to. """ @@ -214,9 +226,13 @@ class ReplyInfo (object): decrypt_success = False sig_success = False pubkey_success = False + key_can_encrypt = False + decrypt_failure = False sig_failure = False sigkey_missing = False + key_cannot_encrypt = False + have_reply_key = False @@ -251,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() @@ -267,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): @@ -348,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() @@ -363,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 @@ -393,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] @@ -402,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) @@ -420,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() @@ -443,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: @@ -571,8 +591,9 @@ def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]): eddymsg_obj should have its payloads split into gpg and non-gpg pieces. Post: - Decryption, verification and key imports occur. the gpg_data member of - PayloadPiece objects get filled in with GPGData objects. + Decryption, verification and key imports occur. the gpg_data members of + PayloadPiece objects get filled in with GPGData objects with some of + their attributes set. """ if eddymsg_obj.multipart == True: @@ -592,9 +613,10 @@ def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]): elif piece.piece_type == TxtType.message: piece.gpg_data = GPGData() - (plaintext_b, sigs, sigkey_missing) = decrypt_block(piece.string, gpgme_ctx) + (plaintext_b, sigs, sigkey_missing, key_cannot_encrypt) = decrypt_block(piece.string, gpgme_ctx) piece.gpg_data.sigkey_missing = sigkey_missing + piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt if plaintext_b: piece.gpg_data.decrypted = True @@ -604,9 +626,10 @@ def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]): continue # if not encrypted, check to see if this is an armored signature. - (plaintext_b, sigs, sigkey_missing) = verify_sig_message(piece.string, gpgme_ctx) + (plaintext_b, sigs, sigkey_missing, key_cannot_encrypt) = verify_sig_message(piece.string, gpgme_ctx) piece.gpg_data.sigkey_missing = sigkey_missing + piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt if plaintext_b: piece.piece_type = TxtType.signature @@ -615,19 +638,23 @@ def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]): piece.gpg_data.plainobj = parse_pgp_mime(plaintext_b, gpgme_ctx) elif piece.piece_type == TxtType.pubkey: - key_fps = add_gpg_key(piece.string, gpgme_ctx) + piece.gpg_data = GPGData() + + (key_fps, key_cannot_encrypt) = add_gpg_key(piece.string, gpgme_ctx) + + piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt if key_fps != []: - piece.gpg_data = GPGData() piece.gpg_data.keys = key_fps elif piece.piece_type == TxtType.detachedsig: piece.gpg_data = GPGData() for prev in prev_parts: - (sig_fps, sigkey_missing) = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx) + (sig_fps, sigkey_missing, key_cannot_encrypt) = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx) piece.gpg_data.sigkey_missing = sigkey_missing + piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt if sig_fps != []: piece.gpg_data.sigs = sig_fps @@ -722,11 +749,12 @@ def prepare_for_reply_message (piece, replyinfo_obj): Post: replyinfo_obj gets updated with decryption status, signing status, a - potential signing key, and posession status of the public key for the - signature. + potential signing key, posession status of the public key for the + signature and encryption capability status if that key is missing. """ - if piece.gpg_data == None or piece.gpg_data.plainobj == None: + if piece.gpg_data.plainobj == None: + replyinfo_obj.decrypt_failure = True return replyinfo_obj.decrypt_success = True @@ -735,18 +763,21 @@ def prepare_for_reply_message (piece, replyinfo_obj): if replyinfo_obj.target_key != None: return - if piece.gpg_data.sigs != []: - replyinfo_obj.target_key = piece.gpg_data.sigs[0] - replyinfo_obj.sig_success = True - get_signed_part = False - - else: + if piece.gpg_data.sigs == []: if piece.gpg_data.sigkey_missing == True: replyinfo_obj.sigkey_missing = True + if piece.gpg_data.key_cannot_encrypt == True: + replyinfo_obj.key_cannot_encrypt = True + # only include a signed message in the reply. get_signed_part = True + else: + replyinfo_obj.target_key = piece.gpg_data.sigs[0] + replyinfo_obj.sig_success = True + get_signed_part = False + flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, get_signed_part) # to catch public keys in encrypted blocks @@ -769,8 +800,9 @@ def prepare_for_reply_pubkey (piece, replyinfo_obj): replyinfo_obj has its fields updated. """ - if piece.gpg_data == None or piece.gpg_data.keys == []: - pass + if piece.gpg_data.keys == []: + if piece.gpg_data.key_cannot_encrypt == True: + replyinfo_obj.key_cannot_encrypt = True else: replyinfo_obj.pubkey_success = True @@ -795,18 +827,25 @@ def prepare_for_reply_sig (piece, replyinfo_obj): replyinfo_obj has its fields updated. """ - if piece.gpg_data == None or piece.gpg_data.sigs == []: + if piece.gpg_data.sigs == []: replyinfo_obj.sig_failure = True if piece.gpg_data.sigkey_missing == True: replyinfo_obj.sigkey_missing = True + if piece.gpg_data.key_cannot_encrypt == True: + replyinfo_obj.key_cannot_encrypt = True + else: replyinfo_obj.sig_success = True if replyinfo_obj.fallback_target_key == None: replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0] + if (piece.piece_type == TxtType.signature): + # to catch public keys in signature blocks + prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj) + def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part): """For creating a string representation of a signed, encrypted part. @@ -857,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 @@ -872,9 +913,7 @@ def get_key_from_fp (replyinfo_obj, gpgme_ctx): gpgme_ctx: the gpgme context Return: - The key object of the key of either the target_key or the fallback one - if .target_key is not set. If the key cannot be loaded, then return - None. + Nothing Pre: Loading a key requires that we have the public key imported. This @@ -884,19 +923,28 @@ def get_key_from_fp (replyinfo_obj, gpgme_ctx): Post: If the key can be loaded, then replyinfo_obj.reply_to_key points to the public key object. If the key cannot be loaded, then the replyinfo_obj - is marked as having no public key available. + is marked as having no public key available. If the key is not capable + of encryption, it will not be used, and replyinfo_obj will be marked + accordingly. """ for key in (replyinfo_obj.target_key, replyinfo_obj.fallback_target_key): if key != None: try: encrypt_to_key = gpgme_ctx.get_key(key) + + except gpgme.GpgmeError: + continue + + 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 return - except gpgme.GpgmeError: - pass + else: + replyinfo_obj.key_cannot_encrypt = True + def write_reply (replyinfo_obj): @@ -923,25 +971,18 @@ def write_reply (replyinfo_obj): reply_plain += replyinfo_obj.replies['greeting'] reply_plain += "\n\n" + 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" - else: + elif replyinfo_obj.decrypt_failure == True: debug('decrypt failure') reply_plain += replyinfo_obj.replies['failed_decrypt'] reply_plain += "\n\n" + if replyinfo_obj.sig_success == True: debug('signature success') reply_plain += replyinfo_obj.replies['sig_success'] @@ -952,6 +993,7 @@ def write_reply (replyinfo_obj): reply_plain += replyinfo_obj.replies['sig_failure'] reply_plain += "\n\n" + if (replyinfo_obj.pubkey_success == True): debug('public key received') reply_plain += replyinfo_obj.replies['public_key_received'] @@ -962,6 +1004,29 @@ def write_reply (replyinfo_obj): reply_plain += replyinfo_obj.replies['no_public_key'] reply_plain += "\n\n" + elif (replyinfo_obj.key_can_encrypt == False) \ + and (replyinfo_obj.key_cannot_encrypt == True): + debug('bad public key') + reply_plain += replyinfo_obj.replies['no_public_key'] + 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'] + reply_plain += "\n\n" + reply_plain += replyinfo_obj.replies['signature'] reply_plain += "\n\n" @@ -980,7 +1045,9 @@ def add_gpg_key (key_block, gpgme_ctx): gpgme_ctx: the gpgme context Returns: - the fingerprint(s) of the imported key(s) + the fingerprint(s) of the imported key(s) which can be used for + encryption, and a boolean marking whether none of the keys are capable + of encryption. """ fp = io.BytesIO(key_block.encode('ascii')) @@ -992,15 +1059,26 @@ def add_gpg_key (key_block, gpgme_ctx): imports = [] key_fingerprints = [] + key_cannot_encrypt = False + + for import_res in imports: + fingerprint = import_res[0] - if imports != []: - for import_ in imports: - fingerprint = import_[0] + try: + key_obj = gpgme_ctx.get_key(fingerprint) + except: + key_obj = None + + if key_obj != None and is_key_usable(key_obj): key_fingerprints += [fingerprint] + key_cannot_encrypt = False debug("added gpg key: " + fingerprint) - return key_fingerprints + elif key_fingerprints == []: + key_cannot_encrypt = True + + return (key_fingerprints, key_cannot_encrypt) def verify_sig_message (msg_block, gpgme_ctx): @@ -1017,10 +1095,11 @@ def verify_sig_message (msg_block, gpgme_ctx): Returns: A tuple containing the plaintext bytes of the signed part, the list of - fingerprints of keys signing the data, and a boolean marking whether - edward is missing all public keys for validating any of the signatures. - If verification failed, perhaps because the message was also encrypted, - then empty results are returned. + fingerprints of encryption-capable keys signing the data, a boolean + marking whether edward is missing all public keys for validating any of + the signatures, and a boolean marking whether all sigs' keys are + incapable of encryption. If verification failed, perhaps because the + message was also encrypted, sensible default values are returned. """ block_b = io.BytesIO(msg_block.encode('ascii')) @@ -1029,22 +1108,13 @@ def verify_sig_message (msg_block, gpgme_ctx): try: sigs = gpgme_ctx.verify(block_b, None, plain_b) except gpgme.GpgmeError: - return ("",[],False) + return ("",[],False,False) plaintext_b = plain_b.getvalue() - sigkey_missing = False - fingerprints = [] - for sig in sigs: - if (sig.summary == 0) or (sig.summary & gpgme.SIGSUM_VALID != 0) or (sig.summary & gpgme.SIGSUM_GREEN != 0): - fingerprints += [sig.fpr] - sigkey_missing = False - break - else: - if (sig.summary & gpgme.SIGSUM_KEY_MISSING != 0): - sigkey_missing = True + (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx) - return (plaintext_b, fingerprints, sigkey_missing) + return (plaintext_b, fingerprints, sigkey_missing, key_cannot_encrypt) def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx): @@ -1058,32 +1128,25 @@ def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx): gpgme_ctx: the gpgme context Returns: - A tuple containging a list of signing fingerprints if the signature - verification was sucessful, and a boolean marking whether edward is - missing all public keys for validating any of the signatures. - Otherwise, a tuple containing an empty list and True are returned. + A tuple containging a list of encryption capable signing fingerprints + if the signature verification was sucessful, a boolean marking whether + edward is missing all public keys for validating any of the signatures, + and a boolean marking whether all signing keys are incapable of + encryption. Otherwise, a tuple containing an empty list and True are + returned. """ detached_sig_fp = io.BytesIO(detached_sig.encode('ascii')) plaintext_fp = io.BytesIO(plaintext_bytes) try: - result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None) + sigs = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None) except gpgme.GpgmeError: - return ([],False) + return ([],False,False) - sigkey_missing = False - sig_fingerprints = [] - for res_ in result: - if (res_.summary == 0) or (res_.summary & gpgme.SIGSUM_VALID != 0) or (res_.summary & gpgme.SIGSUM_GREEN != 0): - sig_fingerprints += [res_.fpr] - sigkey_missing = False - break - else: - if (res_.summary & gpgme.SIGSUM_KEY_MISSING != 0): - sigkey_missing = True + (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx) - return (sig_fingerprints, sigkey_missing) + return (fingerprints, sigkey_missing, key_cannot_encrypt) def decrypt_block (msg_block, gpgme_ctx): @@ -1096,10 +1159,11 @@ def decrypt_block (msg_block, gpgme_ctx): gpgme_ctx: the gpgme context Returns: - A tuple containing plaintext bytes, signatures (if the decryption and - signature verification were successful, respectively), and a boolean - marking whether edward is missing all public keys for validating any of - the signatures. + A tuple containing plaintext bytes, encryption-capable signatures (if + decryption and signature verification were successful, respectively), a + boolean marking whether edward is missing all public keys for + validating any of the signatures, and a boolean marking whether all + signature keys are incapable of encryption. """ block_b = io.BytesIO(msg_block.encode('ascii')) @@ -1108,26 +1172,107 @@ def decrypt_block (msg_block, gpgme_ctx): try: sigs = gpgme_ctx.decrypt_verify(block_b, plain_b) except gpgme.GpgmeError: - return ("",[],False) + return ("",[],False,False) plaintext_b = plain_b.getvalue() + (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx) + + return (plaintext_b, fingerprints, sigkey_missing, key_cannot_encrypt) + + +def get_signature_fp (sigs, gpgme_ctx): + """Selects valid signatures from output of gpgme signature verifying functions + + get_signature_fp returns a list of valid signature fingerprints if those + fingerprints are associated with available keys capable of encryption. + + Args: + sigs: a signature verification result object list + gpgme_ctx: a gpgme context + + Returns: + fingerprints: a list of fingerprints + sigkey_missing: a boolean marking whether public keys are missing for + all available signatures. + key_cannot_encrypt: a boolearn marking whether available public keys are + incapable of encryption. + """ + sigkey_missing = False + key_cannot_encrypt = False fingerprints = [] + for sig in sigs: if (sig.summary == 0) or (sig.summary & gpgme.SIGSUM_VALID != 0) or (sig.summary & gpgme.SIGSUM_GREEN != 0): - fingerprints += [sig.fpr] - sigkey_missing = False - break - else: + try: + key_obj = gpgme_ctx.get_key(sig.fpr) + except: + if fingerprints == []: + sigkey_missing = True + continue + + if is_key_usable(key_obj): + fingerprints += [sig.fpr] + key_cannot_encrypt = False + sigkey_missing = False + + elif fingerprints == []: + key_cannot_encrypt = True + + elif fingerprints == []: if (sig.summary & gpgme.SIGSUM_KEY_MISSING != 0): sigkey_missing = True - return (plaintext_b, fingerprints, sigkey_missing) + 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. @@ -1135,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): @@ -1184,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. @@ -1203,10 +1353,10 @@ def generate_encrypted_mime (plaintext, email_to, email_subject, encrypt_to_key, A string version of the mime message, possibly encrypted and signed. """ - if (encrypt_to_key != None): + plaintext_mime = MIMEText(plaintext) + plaintext_mime.set_charset('utf-8') - plaintext_mime = MIMEText(plaintext) - plaintext_mime.set_charset('utf-8') + if (encrypt_to_key != None): encrypted_text = encrypt_sign_message(plaintext_mime.as_string(), encrypt_to_key, @@ -1231,10 +1381,12 @@ def generate_encrypted_mime (plaintext, email_to, email_subject, encrypt_to_key, message_mime['Content-Disposition'] = 'inline' else: - message_mime = MIMEText(plaintext) - message_mime.set_charset('utf-8') + 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() @@ -1242,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):