updated my personal email address
[edward.git] / edward
diff --git a/edward b/edward
index ca6453b0083159e6ff3bcdbc0528ba51107b545f..d7443927cabc24fa373c66b8019015be5d981dd2 100755 (executable)
--- 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<beginning>.*?)(?P<match>" + pattern +
-                        ")(?P<rest>.*)", 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):
@@ -919,22 +967,22 @@ def write_reply (replyinfo_obj):
 
     reply_plain = ""
 
+    if (replyinfo_obj.pubkey_success == True):
+        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']
         reply_plain += "\n\n"
 
-        if (replyinfo_obj.sig_success == True) and (replyinfo_obj.have_reply_key == True):
-            debug('message quoted')
-            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']
@@ -945,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']
@@ -955,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"
@@ -973,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'))
@@ -985,15 +1059,26 @@ def add_gpg_key (key_block, gpgme_ctx):
         imports = []
 
     key_fingerprints = []
+    key_cannot_encrypt = False
 
-    if imports != []:
-        for import_ in imports:
-            fingerprint = import_[0]
+    for import_res in imports:
+        fingerprint = import_res[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):
@@ -1010,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'))
@@ -1022,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):
@@ -1051,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):
@@ -1089,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'))
@@ -1101,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 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":
 
-def email_to_from_subject (email_bytes):
-    """Returns the values of the email's To:, From: and Subject: fields
+        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.
 
@@ -1128,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):
@@ -1177,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.
@@ -1196,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,
@@ -1224,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()
@@ -1235,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):