updated my personal email address
[edward.git] / edward
diff --git a/edward b/edward
index 5103d8f295c96dfb6bfd1959f04ad341e6a2a7af..d7443927cabc24fa373c66b8019015be5d981dd2 100755 (executable)
--- a/edward
+++ b/edward
 
 Code sourced from these projects:
 
 
 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
 """
 
   * https://git-tails.immerda.ch/whisperback/tree/whisperBack/encryption.py?h=feature/python3
   * http://www.physics.drexel.edu/~wking/code/python/send_pgp_mime
 """
 
-import sys
-import gpgme
 import re
 import io
 import os
 import re
 import io
 import os
+import sys
+import enum
+import gpgme
+import smtplib
 import importlib
 
 import email.parser
 import email.message
 import email.encoders
 
 import importlib
 
 import email.parser
 import email.message
 import email.encoders
 
+from email.mime.text            import MIMEText
 from email.mime.multipart       import MIMEMultipart
 from email.mime.application     import MIMEApplication
 from email.mime.nonmultipart    import MIMENonMultipart
 
 import edward_config
 
 from email.mime.multipart       import MIMEMultipart
 from email.mime.application     import MIMEApplication
 from email.mime.nonmultipart    import MIMENonMultipart
 
 import edward_config
 
-langs = ["de", "el", "en", "fr", "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."""
 
 
 """This list contains the abbreviated names of reply languages available to
 edward."""
 
+class TxtType (enum.Enum):
+    text        = 0
+    message     = 1
+    pubkey      = 2
+    detachedsig = 3
+    signature   = 4
 
 
-match_types =  [('clearsign',
-                '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----'),
-                ('message',
+
+match_pairs =  [(TxtType.message,
                 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
                 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
-                ('pubkey',
+                (TxtType.pubkey,
                 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
                 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
-                ('detachedsig',
+                (TxtType.detachedsig,
                 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
 
 """This list of tuples matches query names with re.search() queries used
                 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
 
 """This list of tuples matches query names with re.search() queries used
@@ -78,16 +86,15 @@ class EddyMsg (object):
     'subparts' points to a list of mime sub-parts if it is a multi-part
     message. Otherwise it points to an empty list.
 
     'subparts' points to a list of mime sub-parts if it is a multi-part
     message. Otherwise it points to an empty list.
 
-    'payload_bytes' contains the raw mime-decoded bytes that haven't been
-    encoded into a character set.
+    'payload_bytes' is a binary representation of the mime part before header
+    removal and message decoding.
 
     'payload_pieces' is a list of objects containing strings that when strung
     together form the fully-decoded string representation of the mime part.
 
 
     'payload_pieces' is a list of objects containing strings that when strung
     together form the fully-decoded string representation of the mime part.
 
-    The 'charset' describes the character set of payload_bytes.
+    The 'filename', 'content_type', 'content_disposition' and
+    'description_list' come from the mime part parameters.
 
 
-    The 'filename', 'content_type' and 'description_list' come from the mime
-    part parameters.
     """
 
     multipart               = False
     """
 
     multipart               = False
@@ -96,9 +103,9 @@ class EddyMsg (object):
     payload_bytes           = None
     payload_pieces          = []
 
     payload_bytes           = None
     payload_pieces          = []
 
-    charset                 = None
     filename                = None
     content_type            = None
     filename                = None
     content_type            = None
+    content_disposition     = None
     description_list        = None
 
 
     description_list        = None
 
 
@@ -109,11 +116,11 @@ class PayloadPiece (object):
     Instances of this class are often strung together within one or more arrays
     pointed to by each instance of the EddyMsg class.
 
     Instances of this class are often strung together within one or more arrays
     pointed to by each instance of the EddyMsg class.
 
-    'piece_type' refers to a string whose value describes the content of
-    'string'.  Examples include "pubkey", for public keys, and "message", for
-    encrypted data (or armored signatures until they are known to be such.) The
-    names derive from the header and footer of each of these ascii-encoded gpg
-    blocks.
+    'piece_type' refers to an enum whose value describes the content of
+    'string'.  Examples include TxtType.pubkey, for public keys, and
+    TxtType.message, for encrypted data (or armored signatures until they are
+    known to be such.) Some of the names derive from the header and footer of
+    each of these ascii-encoded gpg blocks.
 
     'string' contains some string of text, such as non-GPG text, an encrypted
     block of text, a signature, or a public key.
 
     'string' contains some string of text, such as non-GPG text, an encrypted
     block of text, a signature, or a public key.
@@ -141,6 +148,12 @@ class GPGData (object):
 
     'sigs' is a list of fingerprints of keys used to sign the data in plainobj.
 
 
     'sigs' is a list of fingerprints of keys used to sign the data in plainobj.
 
+    '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.
     """
 
     'keys' is a list of fingerprints of keys obtained in public key blocks.
     """
 
@@ -148,6 +161,8 @@ class GPGData (object):
 
     plainobj                = None
     sigs                    = []
 
     plainobj                = None
     sigs                    = []
+    sigkey_missing          = False
+    key_cannot_encrypt      = False
     keys                    = []
 
 
     keys                    = []
 
 
@@ -170,42 +185,55 @@ class ReplyInfo (object):
     unencrypted data; alternatively it may be a public key attached to the
     message.
 
     unencrypted data; alternatively it may be a public key attached to the
     message.
 
+    'encrypt_to_key' the key object to use when encrypting edward's reply
+
     'msg_to_quote' refers to the part of a message which edward should quote in
     his reply. This should remain as None if there was no encrypted and singed
     part. This is to avoid making edward a service for decrypting other
     people's messages to edward.
 
     'msg_to_quote' refers to the part of a message which edward should quote in
     his reply. This should remain as None if there was no encrypted and singed
     part. This is to avoid making edward a service for decrypting other
     people's messages to edward.
 
-    'success_decrypt' is set to True if edward could decrypt part of the
+    'decrypt_success' is set to True if edward could decrypt part of the
     message.
 
     message.
 
-    'failed_decrypt' is set to True if edward failed to decrypt part of the
-    message.
+    'sig_success' is set to True if edward could to some extent verify the
+    signature of a signed part of the message to edward.
 
 
-    'publick_key_received' is set to True if edward successfully imported a
-    public key.
+    'key_can_encrypt' is set to True if a key which can be encrypted to has
+    been found.
 
 
-    'no_public_key' is set to True if edward doesn't have a key to encrypt to
-    when replying to the user.
+    'sig_failure' is set to True if edward could not verify a siganture.
 
 
-    'sig_success' is set to True if edward could to some extent verify the
-    signature of a signed part of the message to edward.
+    'pubkey_success' is set to True if edward successfully imported a public
+    key.
 
 
-    'sig_failure' is set to True if edward failed to some extent verify the
-    signature of a signed part of the message to edward.
+    '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.
     """
 
     replies                 = None
 
     target_key              = None
     fallback_target_key     = None
     """
 
     replies                 = None
 
     target_key              = None
     fallback_target_key     = None
+    encrypt_to_key          = None
     msg_to_quote            = ""
 
     msg_to_quote            = ""
 
-    success_decrypt         = False
-    failed_decrypt          = False
-    public_key_received     = False
-    no_public_key           = False
+    decrypt_success         = False
     sig_success             = False
     sig_success             = False
+    pubkey_success          = False
+    key_can_encrypt         = False
+
+    decrypt_failure         = False
     sig_failure             = False
     sig_failure             = False
+    sigkey_missing          = False
+    key_cannot_encrypt      = False
+
+    have_reply_key          = False
 
 
 def main ():
 
 
 def main ():
@@ -237,29 +265,37 @@ def main ():
         implied by the To: address in the original email.
     """
 
         implied by the To: address in the original email.
     """
 
-    handle_args()
+    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)
 
 
     gpgme_ctx = get_gpg_context(edward_config.gnupghome,
                               edward_config.sign_with_key)
 
-    email_text = sys.stdin.read()
-    email_struct = parse_pgp_mime(email_text, gpgme_ctx)
+    # 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_text)
-    lang = import_lang(email_to)
+    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()
     replyinfo_obj.replies = lang.replies
 
     prepare_for_reply(email_struct, replyinfo_obj)
 
     replyinfo_obj = ReplyInfo()
     replyinfo_obj.replies = lang.replies
 
     prepare_for_reply(email_struct, replyinfo_obj)
-    encrypt_to_key = get_key_from_fp(replyinfo_obj, gpgme_ctx)
+    get_key_from_fp(replyinfo_obj, gpgme_ctx)
     reply_plaintext = write_reply(replyinfo_obj)
 
     reply_plaintext = write_reply(replyinfo_obj)
 
-    reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \
-                                         email_subject, encrypt_to_key,
+    reply_mime = generate_encrypted_mime(reply_plaintext, email_reply_to, reply_from, \
+                                         email_subject, replyinfo_obj.encrypt_to_key,
                                          gpgme_ctx)
 
                                          gpgme_ctx)
 
-    print(reply_mime)
+    if print_reply_only == True:
+        print(reply_mime)
+    else:
+        send_reply(reply_mime, email_reply_to, reply_from)
 
 
 def get_gpg_context (gnupghome, sign_with_key_fp):
 
 
 def get_gpg_context (gnupghome, sign_with_key_fp):
@@ -287,7 +323,7 @@ def get_gpg_context (gnupghome, sign_with_key_fp):
 
     try:
         sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
 
     try:
         sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
-    except:
+    except gpgme.GpgmeError:
         error("unable to load signing key. is the gnupghome "
                 + "and signing key properly set in the edward_config.py?")
         exit(1)
         error("unable to load signing key. is the gnupghome "
                 + "and signing key properly set in the edward_config.py?")
         exit(1)
@@ -297,7 +333,7 @@ def get_gpg_context (gnupghome, sign_with_key_fp):
     return gpgme_ctx
 
 
     return gpgme_ctx
 
 
-def parse_pgp_mime (email_text, gpgme_ctx):
+def parse_pgp_mime (email_bytes, gpgme_ctx):
     """Parses the email for mime payloads and decrypts/verfies signatures.
 
     This function creates a representation of a mime or plaintext email with
     """Parses the email for mime payloads and decrypts/verfies signatures.
 
     This function creates a representation of a mime or plaintext email with
@@ -306,7 +342,7 @@ def parse_pgp_mime (email_text, gpgme_ctx):
     does some very basic signature verification on those parts.
 
     Args:
     does some very basic signature verification on those parts.
 
     Args:
-        email_text: an email message in string format
+        email_bytes: an email message in byte string format
         gpgme_ctx:  a gpgme context
 
     Returns:
         gpgme_ctx:  a gpgme context
 
     Returns:
@@ -317,7 +353,7 @@ def parse_pgp_mime (email_text, gpgme_ctx):
         imported payloads
     """
 
         imported payloads
     """
 
-    email_struct = email.parser.Parser().parsestr(email_text)
+    email_struct = email.parser.BytesParser().parsebytes(email_bytes)
 
     eddymsg_obj = parse_mime(email_struct)
     split_payloads(eddymsg_obj)
 
     eddymsg_obj = parse_mime(email_struct)
     split_payloads(eddymsg_obj)
@@ -333,14 +369,14 @@ def parse_mime(msg_struct):
     each sub-part is also a EddyMsg instance.
 
     Args:
     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.
     """
 
             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()
 
     if msg_struct.is_multipart() == True:
         payloads = msg_struct.get_payload()
@@ -348,13 +384,10 @@ def parse_mime(msg_struct):
         eddymsg_obj.multipart = True
         eddymsg_obj.subparts = list(map(parse_mime, payloads))
 
         eddymsg_obj.multipart = True
         eddymsg_obj.subparts = list(map(parse_mime, payloads))
 
-    else:
-        eddymsg_obj = get_subpart_data(msg_struct)
-
     return eddymsg_obj
 
 
     return eddymsg_obj
 
 
-def scan_and_split (payload_piece, match_type, pattern):
+def scan_and_split (payload_piece, match_name, pattern):
     """This splits the payloads of an EddyMsg object into GPG and text parts.
 
     An EddyMsg object's payload_pieces starts off as a list containing a single
     """This splits the payloads of an EddyMsg object into GPG and text parts.
 
     An EddyMsg object's payload_pieces starts off as a list containing a single
@@ -364,7 +397,7 @@ def scan_and_split (payload_piece, match_type, pattern):
 
     Args:
         payload_piece: a single payload or a split part of a payload
 
     Args:
         payload_piece: a single payload or a split part of a payload
-        match_type: the type of data to try to spit out from the payload piece
+        match_name: the type of data to try to spit out from the payload piece
         pattern: the search pattern to be used for finding that type of data
 
     Returns:
         pattern: the search pattern to be used for finding that type of data
 
     Returns:
@@ -374,12 +407,11 @@ def scan_and_split (payload_piece, match_type, pattern):
     """
 
     # don't try to re-split pieces containing gpg data
     """
 
     # don't try to re-split pieces containing gpg data
-    if payload_piece.piece_type != "text":
+    if payload_piece.piece_type != TxtType.text:
         return [payload_piece]
 
     flags = re.DOTALL | re.MULTILINE
         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]
 
     if matches == None:
         pieces = [payload_piece]
@@ -387,54 +419,61 @@ def scan_and_split (payload_piece, match_type, pattern):
     else:
 
         beginning               = PayloadPiece()
     else:
 
         beginning               = PayloadPiece()
-        beginning.string        = matches.group('beginning')
+        beginning.string        = payload_piece.string[:matches.start()]
         beginning.piece_type    = payload_piece.piece_type
 
         match                   = PayloadPiece()
         beginning.piece_type    = payload_piece.piece_type
 
         match                   = PayloadPiece()
-        match.string            = matches.group('match')
-        match.piece_type        = match_type
+        match.string            = payload_piece.string[matches.start():matches.end()]
+        match.piece_type        = match_name
 
         rest                    = PayloadPiece()
 
         rest                    = PayloadPiece()
-        rest.string             = matches.group('rest')
+        rest.string             = payload_piece.string[matches.end():]
         rest.piece_type         = payload_piece.piece_type
 
         rest.piece_type         = payload_piece.piece_type
 
-        more_pieces = scan_and_split(rest, match_type, pattern)
+        more_pieces = scan_and_split(rest, match_name, pattern)
         pieces = [beginning, match ] + more_pieces
 
     return pieces
 
 
 def get_subpart_data (part):
         pieces = [beginning, match ] + more_pieces
 
     return pieces
 
 
 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:
 
     Args:
-        part: a non-multi-part mime.parser.Parser() object
+        part: an email.parser.BytesParser() object
 
     Returns:
 
     Returns:
-        a single-part EddyMsg() object
+        an EddyMsg() object
     """
 
     obj = EddyMsg()
 
     """
 
     obj = EddyMsg()
 
-    obj.charset             = part.get_content_charset()
-    obj.payload_bytes       = part.get_payload(decode=True)
+    mime_decoded_bytes      = part.get_payload(decode=True)
+    charset                 = part.get_content_charset()
+
+    # your guess is as good as a-myy-ee-ine...
+    if charset == None:
+        charset = 'utf-8'
+
+    payload_string          = part.as_string()
+    if payload_string != None:
+        # 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.filename            = part.get_filename()
     obj.content_type        = part.get_content_type()
+    obj.content_disposition = part['content-disposition']
     obj.description_list    = part['content-description']
 
     obj.description_list    = part['content-description']
 
-    # your guess is as good as a-myy-ee-ine...
-    if obj.charset == None:
-        obj.charset = 'utf-8'
-
-    if obj.payload_bytes != None:
+    if mime_decoded_bytes != None:
         try:
             payload = PayloadPiece()
         try:
             payload = PayloadPiece()
-            payload.string = obj.payload_bytes.decode(obj.charset)
-            payload.piece_type = 'text'
+            payload.string = mime_decoded_bytes.decode(charset)
+            payload.piece_type = TxtType.text
 
             obj.payload_pieces = [payload]
         except UnicodeDecodeError:
 
             obj.payload_pieces = [payload]
         except UnicodeDecodeError:
@@ -491,11 +530,11 @@ def split_payloads (eddymsg_obj):
         The EddyMsg object's payloads are all split into GPG and non-GPG parts.
     """
 
         The EddyMsg object's payloads are all split into GPG and non-GPG parts.
     """
 
-    for match_type in match_types:
-        do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_type)
+    for match_pair in match_pairs:
+        do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_pair)
 
 
 
 
-def split_payload_pieces (eddymsg_obj, match_type):
+def split_payload_pieces (eddymsg_obj, match_pair):
     """A helper function for split_payloads(); works on PayloadPiece objects.
 
     This function splits up PayloadPiece objects into multipe PayloadPiece
     """A helper function for split_payloads(); works on PayloadPiece objects.
 
     This function splits up PayloadPiece objects into multipe PayloadPiece
@@ -504,7 +543,7 @@ def split_payload_pieces (eddymsg_obj, match_type):
 
     Args:
         eddymsg_obj: a single-part EddyMsg object.
 
     Args:
         eddymsg_obj: a single-part EddyMsg object.
-        match_type: a tuple from the match_types list, which specifies a match
+        match_pair: a tuple from the match_pairs list, which specifies a match
             name and a match pattern.
 
     Returns:
             name and a match pattern.
 
     Returns:
@@ -516,10 +555,10 @@ def split_payload_pieces (eddymsg_obj, match_type):
 
     Post:
         The EddyMsg object's payload piece(s) are split into a list of pieces
 
     Post:
         The EddyMsg object's payload piece(s) are split into a list of pieces
-        if matches of the match_type are found.
+        if matches of the match_pair are found.
     """
 
     """
 
-    (match_name, pattern) = match_type
+    (match_name, pattern) = match_pair
 
     new_pieces_list = []
     for piece in eddymsg_obj.payload_pieces:
 
     new_pieces_list = []
     for piece in eddymsg_obj.payload_pieces:
@@ -552,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:
         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:
     """
 
     if eddymsg_obj.multipart == True:
@@ -566,53 +606,57 @@ def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
 
     for piece in eddymsg_obj.payload_pieces:
 
 
     for piece in eddymsg_obj.payload_pieces:
 
-        if piece.piece_type == "text":
+        if piece.piece_type == TxtType.text:
             # don't transform the plaintext.
             pass
 
             # don't transform the plaintext.
             pass
 
-        elif piece.piece_type == "message":
-            (plaintext, sigs) = decrypt_block(piece.string, gpgme_ctx)
+        elif piece.piece_type == TxtType.message:
+            piece.gpg_data = GPGData()
 
 
-            if plaintext:
-                piece.gpg_data = GPGData()
+            (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
                 piece.gpg_data.sigs = sigs
                 # recurse!
                 piece.gpg_data.decrypted = True
                 piece.gpg_data.sigs = sigs
                 # recurse!
-                piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
+                piece.gpg_data.plainobj = parse_pgp_mime(plaintext_b, gpgme_ctx)
                 continue
 
             # if not encrypted, check to see if this is an armored signature.
                 continue
 
             # if not encrypted, check to see if this is an armored signature.
-            (plaintext, sigs) = verify_sig_message(piece.string, gpgme_ctx)
+            (plaintext_b, sigs,  sigkey_missing, key_cannot_encrypt) = verify_sig_message(piece.string, gpgme_ctx)
 
 
-            if plaintext:
-                piece.piece_type = "signature"
-                piece.gpg_data = GPGData()
+            piece.gpg_data.sigkey_missing = sigkey_missing
+            piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
+
+            if plaintext_b:
+                piece.piece_type = TxtType.signature
                 piece.gpg_data.sigs = sigs
                 # recurse!
                 piece.gpg_data.sigs = sigs
                 # recurse!
-                piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
+                piece.gpg_data.plainobj = parse_pgp_mime(plaintext_b, gpgme_ctx)
+
+        elif piece.piece_type == TxtType.pubkey:
+            piece.gpg_data = GPGData()
 
 
-        elif piece.piece_type == "pubkey":
-            key_fps = add_gpg_key(piece.string, gpgme_ctx)
+            (key_fps, key_cannot_encrypt) = add_gpg_key(piece.string, gpgme_ctx)
+
+            piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
 
             if key_fps != []:
 
             if key_fps != []:
-                piece.gpg_data = GPGData()
                 piece.gpg_data.keys = key_fps
 
                 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 == TxtType.detachedsig:
+            piece.gpg_data = GPGData()
 
 
-        elif piece.piece_type == "detachedsig":
             for prev in prev_parts:
             for prev in prev_parts:
-                payload_bytes = prev.payload_bytes
-                sig_fps = verify_detached_signature(piece.string, 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 != []:
 
                 if sig_fps != []:
-                    piece.gpg_data = GPGData()
                     piece.gpg_data.sigs = sig_fps
                     piece.gpg_data.plainobj = prev
                     break
                     piece.gpg_data.sigs = sig_fps
                     piece.gpg_data.plainobj = prev
                     break
@@ -670,19 +714,18 @@ def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
     """
 
     for piece in eddymsg_obj.payload_pieces:
     """
 
     for piece in eddymsg_obj.payload_pieces:
-        if piece.piece_type == "text":
+        if piece.piece_type == TxtType.text:
             # don't quote the plaintext part.
             pass
 
             # don't quote the plaintext part.
             pass
 
-        elif piece.piece_type == "message":
+        elif piece.piece_type == TxtType.message:
             prepare_for_reply_message(piece, replyinfo_obj)
 
             prepare_for_reply_message(piece, replyinfo_obj)
 
-        elif piece.piece_type == "pubkey":
+        elif piece.piece_type == TxtType.pubkey:
             prepare_for_reply_pubkey(piece, replyinfo_obj)
 
             prepare_for_reply_pubkey(piece, replyinfo_obj)
 
-        elif (piece.piece_type == "clearsign") \
-            or (piece.piece_type == "detachedsig") \
-            or (piece.piece_type == "signature"):
+        elif (piece.piece_type == TxtType.detachedsig) \
+            or (piece.piece_type == TxtType.signature):
                     prepare_for_reply_sig(piece, replyinfo_obj)
 
 
                     prepare_for_reply_sig(piece, replyinfo_obj)
 
 
@@ -690,44 +733,52 @@ def prepare_for_reply_message (piece, replyinfo_obj):
     """Helper function for prepare_for_reply()
 
     This function is called when the piece_type of a payload piece is
     """Helper function for prepare_for_reply()
 
     This function is called when the piece_type of a payload piece is
-    "message", or GPG Message block. This should be encrypted text. If the
-    encryted block is signed, a sig will be attached to .target_key unless
-    there is already one there.
+    TxtType.message, or GPG Message block. This should be encrypted text. If
+    the encryted block is correclty signed, a sig will be attached to
+    .target_key unless there is already one there.
 
     Args:
         piece: a PayloadPiece object.
         replyinfo_obj: object which gets updated with decryption status, etc.
 
 
     Args:
         piece: a PayloadPiece object.
         replyinfo_obj: object which gets updated with decryption status, etc.
 
-
     Returns:
         Nothing
 
     Pre:
     Returns:
         Nothing
 
     Pre:
-        the piece.payload_piece value should be "message".
+        the piece.payload_piece value should be TxtType.message.
 
     Post:
 
     Post:
-        replyinfo_obj gets updated with decryption status, signing status and a
-        potential signing key.
+        replyinfo_obj gets updated with decryption status, signing status, a
+        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:
-        replyinfo_obj.failed_decrypt = True
+    if piece.gpg_data.plainobj == None:
+        replyinfo_obj.decrypt_failure = True
         return
 
         return
 
-    replyinfo_obj.success_decrypt = True
+    replyinfo_obj.decrypt_success = True
 
     # we already have a key (and a message)
     if replyinfo_obj.target_key != None:
         return
 
 
     # we already have a key (and a message)
     if replyinfo_obj.target_key != None:
         return
 
-    if piece.gpg_data.sigs != []:
-        replyinfo_obj.target_key = piece.gpg_data.sigs[0]
-        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
 
         # only include a signed message in the reply.
         get_signed_part = True
 
-    replyinfo_obj.msg_to_quote = flatten_decrypted_payloads(piece.gpg_data.plainobj, get_signed_part)
+    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
     prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
 
     # to catch public keys in encrypted blocks
     prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
@@ -743,19 +794,20 @@ def prepare_for_reply_pubkey (piece, replyinfo_obj):
         replyinfo_obj: a ReplyInfo object
 
     Pre:
         replyinfo_obj: a ReplyInfo object
 
     Pre:
-        piece.piece_type should be set to "pubkey".
+        piece.piece_type should be set to TxtType.pubkey .
 
     Post:
         replyinfo_obj has its fields updated.
     """
 
 
     Post:
         replyinfo_obj has its fields updated.
     """
 
-    if piece.gpg_data == None or piece.gpg_data.keys == []:
-        replyinfo_obj.no_public_key = True
+    if piece.gpg_data.keys == []:
+        if piece.gpg_data.key_cannot_encrypt == True:
+            replyinfo_obj.key_cannot_encrypt = True
     else:
     else:
-        replyinfo_obj.public_key_received = True
+        replyinfo_obj.pubkey_success = True
 
 
-        if replyinfo_obj.fallback_target_key == None:
-            replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
+        # prefer public key as a fallback for the encrypted reply
+        replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
 
 
 def prepare_for_reply_sig (piece, replyinfo_obj):
 
 
 def prepare_for_reply_sig (piece, replyinfo_obj):
@@ -768,257 +820,541 @@ def prepare_for_reply_sig (piece, replyinfo_obj):
         replyinfo_obj: a ReplyInfo object
 
     Pre:
         replyinfo_obj: a ReplyInfo object
 
     Pre:
-        piece.piece_type should be set to "clearsign", "signature", or
-        "detachedsig".
+        piece.piece_type should be set to TxtType.signature, or
+        TxtType.detachedsig .
 
     Post:
         replyinfo_obj has its fields updated.
     """
 
 
     Post:
         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
         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]
 
     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.
 
 
+    When given a decrypted payload, it will add either the plaintext or signed
+    plaintext to the reply message, depeding on 'get_signed_part'. This is
+    useful for ensuring that the reply message only comes from a signed and
+    ecrypted GPG message. It also sets the target_key for encrypting the reply
+    if it's told to get signed text only.
+
+    Args:
+        eddymsg_obj: the message in EddyMsg format created by decrypting GPG
+            text
+        replyinfo_obj: a ReplyInfo object for holding the message to quote and
+            the target_key to encrypt to.
+        get_signed_part: True if we should only include text that contains a
+            further signature. If False, then include plain text.
+
+    Returns:
+        Nothing
 
 
-def flatten_decrypted_payloads (eddymsg_obj, get_signed_part):
+    Pre:
+        The EddyMsg instance passed in should be a piece.gpg_data.plainobj
+        which represents decrypted text. It may or may not be signed on that
+        level.
 
 
-    flat_string = ""
+    Post:
+        the ReplyInfo instance may have a new 'target_key' set and its
+        'msg_to_quote' will be updated with (possibly signed) plaintext, if any
+        could be found.
+    """
 
     if eddymsg_obj == None:
 
     if eddymsg_obj == None:
-        return ""
+        return
 
     # recurse on multi-part mime
     if eddymsg_obj.multipart == True:
         for sub in eddymsg_obj.subparts:
 
     # recurse on multi-part mime
     if eddymsg_obj.multipart == True:
         for sub in eddymsg_obj.subparts:
-            flat_string += flatten_decrypted_payloads (sub, get_signed_part)
-
-        return flat_string
+            flatten_decrypted_payloads(sub, replyinfo_obj, get_signed_part)
 
     for piece in eddymsg_obj.payload_pieces:
         if (get_signed_part):
 
     for piece in eddymsg_obj.payload_pieces:
         if (get_signed_part):
-            if ((piece.piece_type == "clearsign") \
-                    or (piece.piece_type == "detachedsig") \
-                    or (piece.piece_type == "signature")) \
-                    and (piece.gpg_data != None):
-                        # FIXME: the key used to sign this message needs to be the one that is used for the encrypted reply.
-                        flat_string += flatten_decrypted_payloads (piece.gpg_data.plainobj, False)
+            if ((piece.piece_type == TxtType.detachedsig) \
+                    or (piece.piece_type == TxtType.signature)) \
+                    and (piece.gpg_data != None) \
+                    and (piece.gpg_data.plainobj != None):
+                        flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, False)
+                        replyinfo_obj.target_key = piece.gpg_data.sigs[0]
                         break
         else:
                         break
         else:
-            if piece.piece_type == "text":
-                flat_string += piece.string
-
-    return flat_string
+            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
 
 
 def get_key_from_fp (replyinfo_obj, gpgme_ctx):
 
 
 def get_key_from_fp (replyinfo_obj, gpgme_ctx):
+    """Obtains a public key object from a key fingerprint
 
 
-    if replyinfo_obj.target_key == None:
-        replyinfo_obj.target_key = replyinfo_obj.fallback_target_key
+    If the .target_key is not set, then we use .fallback_target_key, if
+    available.
 
 
-    if replyinfo_obj.target_key != None:
-        try:
-            encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key)
-            return encrypt_to_key
+    Args:
+        replyinfo_obj: ReplyInfo instance
+        gpgme_ctx: the gpgme context
 
 
-        except:
-            pass
+    Return:
+        Nothing
+
+    Pre:
+        Loading a key requires that we have the public key imported. This
+        requires that they email contains the pub key block, or that it was
+        previously sent to edward.
 
 
-    # no available key to use
-    replyinfo_obj.target_key = None
-    replyinfo_obj.fallback_target_key = None
+    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. 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
 
 
-    replyinfo_obj.no_public_key = True
-    replyinfo_obj.public_key_received = False
+            else:
+                replyinfo_obj.key_cannot_encrypt = True
 
 
-    return None
 
 
 def write_reply (replyinfo_obj):
 
 
 def write_reply (replyinfo_obj):
+    """Write the reply email body about the GPG successes/failures.
+
+    The reply is about whether decryption, sig verification and key
+    import/loading was successful or failed. If text was successfully decrypted
+    and verified, then the first instance of such text will be included in
+    quoted form.
+
+    Args:
+        replyinfo_obj: contains details of GPG processing status
+
+    Returns:
+        the plaintext message to be sent to the user
+
+    Pre:
+        replyinfo_obj should be populated with info about GPG processing status.
+    """
 
     reply_plain = ""
 
 
     reply_plain = ""
 
-    if replyinfo_obj.success_decrypt == True:
-        reply_plain += replyinfo_obj.replies['success_decrypt']
+    if (replyinfo_obj.pubkey_success == True):
+        reply_plain += replyinfo_obj.replies['greeting']
+        reply_plain += "\n\n"
 
 
-        if replyinfo_obj.no_public_key == False:
-            quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
-            reply_plain += quoted_text
 
 
-    elif replyinfo_obj.failed_decrypt == True:
+    if replyinfo_obj.decrypt_success == True:
+        debug('decrypt success')
+        reply_plain += replyinfo_obj.replies['success_decrypt']
+        reply_plain += "\n\n"
+
+    elif replyinfo_obj.decrypt_failure == True:
+        debug('decrypt failure')
         reply_plain += replyinfo_obj.replies['failed_decrypt']
         reply_plain += replyinfo_obj.replies['failed_decrypt']
+        reply_plain += "\n\n"
 
 
     if replyinfo_obj.sig_success == True:
 
 
     if replyinfo_obj.sig_success == True:
-        reply_plain += "\n\n"
+        debug('signature success')
         reply_plain += replyinfo_obj.replies['sig_success']
         reply_plain += replyinfo_obj.replies['sig_success']
+        reply_plain += "\n\n"
 
     elif replyinfo_obj.sig_failure == True:
 
     elif replyinfo_obj.sig_failure == True:
-        reply_plain += "\n\n"
+        debug('signature failure')
         reply_plain += replyinfo_obj.replies['sig_failure']
         reply_plain += replyinfo_obj.replies['sig_failure']
+        reply_plain += "\n\n"
 
 
 
 
-    if replyinfo_obj.public_key_received == True:
-        reply_plain += "\n\n"
+    if (replyinfo_obj.pubkey_success == True):
+        debug('public key received')
         reply_plain += replyinfo_obj.replies['public_key_received']
         reply_plain += replyinfo_obj.replies['public_key_received']
+        reply_plain += "\n\n"
 
 
-    elif replyinfo_obj.no_public_key == True:
+    elif (replyinfo_obj.sigkey_missing == True):
+        debug('no public key')
+        reply_plain += replyinfo_obj.replies['no_public_key']
         reply_plain += "\n\n"
         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 += 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 += "\n\n"
     reply_plain += replyinfo_obj.replies['signature']
     reply_plain += replyinfo_obj.replies['signature']
+    reply_plain += "\n\n"
 
     return reply_plain
 
 
 def add_gpg_key (key_block, gpgme_ctx):
 
     return reply_plain
 
 
 def add_gpg_key (key_block, gpgme_ctx):
+    """Adds a GPG pubkey to the local keystore
+
+    This adds keys received through email into the key store so they can be
+    used later.
+
+    Args:
+        key_block: the string form of the ascii-armored public key block
+        gpgme_ctx: the gpgme context
+
+    Returns:
+        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'))
 
 
     fp = io.BytesIO(key_block.encode('ascii'))
 
-    result = gpgme_ctx.import_(fp)
-    imports = result.imports
+    try:
+        result = gpgme_ctx.import_(fp)
+        imports = result.imports
+    except gpgme.GpgmeError:
+        imports = []
 
     key_fingerprints = []
 
     key_fingerprints = []
+    key_cannot_encrypt = False
+
+    for import_res in imports:
+        fingerprint = import_res[0]
+
+        try:
+            key_obj = gpgme_ctx.get_key(fingerprint)
+        except:
+            key_obj = None
 
 
-    if imports != []:
-        for import_ in imports:
-            fingerprint = import_[0]
+        if key_obj != None and is_key_usable(key_obj):
             key_fingerprints += [fingerprint]
             key_fingerprints += [fingerprint]
+            key_cannot_encrypt = False
 
             debug("added gpg key: " + fingerprint)
 
 
             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):
 
 
 def verify_sig_message (msg_block, gpgme_ctx):
+    """Verifies the signature of a signed, ascii-armored block of text.
+
+    It encodes the string into ascii, since binary GPG files are currently
+    unsupported, and alternative, the ascii-armored format is encodable into
+    ascii.
+
+    Args:
+        msg_block: a GPG Message block in string form. It may be encrypted or
+            not. If it is encrypted, it will return empty results.
+        gpgme_ctx: the gpgme context
+
+    Returns:
+        A tuple containing the plaintext bytes of the signed part, the list of
+        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'))
     plain_b = io.BytesIO()
 
     try:
         sigs = gpgme_ctx.verify(block_b, None, plain_b)
 
     block_b = io.BytesIO(msg_block.encode('ascii'))
     plain_b = io.BytesIO()
 
     try:
         sigs = gpgme_ctx.verify(block_b, None, plain_b)
-    except:
-        return ("",[])
-
-    plaintext = plain_b.getvalue().decode('utf-8')
-
-    fingerprints = []
-    for sig in sigs:
-        fingerprints += [sig.fpr]
-    return (plaintext, fingerprints)
-
+    except gpgme.GpgmeError:
+        return ("",[],False,False)
 
 
-def verify_clear_signature (sig_block, gpgme_ctx):
+    plaintext_b = plain_b.getvalue()
 
 
-    # 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()
+    (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx)
 
 
-    result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
+    return (plaintext_b, fingerprints, sigkey_missing, key_cannot_encrypt)
 
 
-    # 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]
+def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
+    """Verifies the signature of a detached signature.
 
 
-    return plaintext, sig_fingerprints
+    This requires the signature part and the signed part as separate arguments.
 
 
+    Args:
+        detached_sig: the signature part of the detached signature
+        plaintext_bytes: the byte form of the message being signed.
+        gpgme_ctx: the gpgme context
 
 
-def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
+    Returns:
+        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)
 
     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)
+    try:
+        sigs = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
+    except gpgme.GpgmeError:
+        return ([],False,False)
 
 
-    sig_fingerprints = []
-    for res_ in result:
-        sig_fingerprints += [res_.fpr]
+    (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx)
 
 
-    return sig_fingerprints
+    return (fingerprints, sigkey_missing, key_cannot_encrypt)
 
 
 def decrypt_block (msg_block, gpgme_ctx):
 
 
 def decrypt_block (msg_block, gpgme_ctx):
+    """Decrypts a block of GPG text and verifies any included sigatures.
+
+    Some encypted messages have embeded signatures, so those are verified too.
+
+    Args:
+        msg_block: the encrypted(/signed) text
+        gpgme_ctx: the gpgme context
+
+    Returns:
+        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'))
     plain_b = io.BytesIO()
 
     try:
         sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
 
     block_b = io.BytesIO(msg_block.encode('ascii'))
     plain_b = io.BytesIO()
 
     try:
         sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
-    except:
-        return ("",[])
+    except gpgme.GpgmeError:
+        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.
 
 
-    plaintext = plain_b.getvalue().decode('utf-8')
+    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 = []
     fingerprints = []
+
     for sig in sigs:
     for sig in sigs:
-        fingerprints += [sig.fpr]
-    return (plaintext, fingerprints)
+        if (sig.summary == 0) or (sig.summary & gpgme.SIGSUM_VALID != 0) or (sig.summary & gpgme.SIGSUM_GREEN != 0):
+            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
 
 
-def choose_reply_encryption_key (gpgme_ctx, fingerprints):
+            elif fingerprints == []:
+                key_cannot_encrypt = True
 
 
-    reply_key = None
-    for fp in fingerprints:
-        try:
-            key = gpgme_ctx.get_key(fp)
+        elif fingerprints == []:
+            if (sig.summary & gpgme.SIGSUM_KEY_MISSING != 0):
+                sigkey_missing = True
 
 
-            if (key.can_encrypt == True):
-                reply_key = key
-                break
-        except:
-            continue
+    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":
+
+        return
+
+    debug("autoreply")
+    exit(0)
 
 
 
 
-    return reply_key
+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.
 
 
-def email_to_from_subject (email_text):
+    Args:
+        email_bytes: the byte string form of the email
+
+    Returns:
+        the email To:, Reply-To: (or From:), and Subject: fields as strings
+    """
 
 
-    email_struct = email.parser.Parser().parsestr(email_text)
+    email_struct = email.parser.BytesHeaderParser().parsebytes(email_bytes)
 
     email_to        = email_struct['To']
     email_from      = email_struct['From']
 
     email_to        = email_struct['To']
     email_from      = email_struct['From']
+    email_reply_to  = email_struct['Reply-To']
+
     email_subject   = email_struct['Subject']
 
     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):
+    """Imports language file for i18n support; makes reply from address
 
 
+    The language imported depends on the To: address of the email received by
+    edward. an -en ending implies the English language, whereas a -ja ending
+    implies Japanese. The list of supported languages is listed in the 'langs'
+    list at the beginning of the program. This function also chooses the
+    language-dependent address which can be used as the From address in the
+    reply email.
 
 
-def import_lang(email_to):
+    Args:
+        email_to: the string containing the email address that the mail was
+            sent to.
+        hostname: the hostname part of the reply email's from address
+
+    Returns:
+        the reference to the imported language module. The only variable in
+        this file is the 'replies' dictionary.
+    """
+
+    # default
+    use_lang = "en"
 
     if email_to != None:
         for lang in langs:
             if "edward-" + lang in 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)
+                use_lang = lang
+                break
+
+    lang_mod_name = "lang." + re.sub('-', '_', use_lang)
+    lang_module = importlib.import_module(lang_mod_name)
+
+    reply_from = "edward-" + use_lang + "@" + hostname
 
 
-                return language
+    return lang_module, reply_from
 
 
-    return importlib.import_module("lang.en")
 
 
+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.
 
 
-def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
-                    gpgme_ctx):
+    If the encrypt_key is included, then the email is encrypted and signed.
+    Otherwise it is unencrypted.
 
 
-    # quoted printable encoding lets most ascii characters look normal
-    # before the mime message is decoded.
-    char_set = email.charset.Charset("utf-8")
-    char_set.body_encoding = email.charset.QP
+    Args:
+        plaintext: the plaintext body of the message to create.
+        email_to: the email address to reply to
+        email_subject: the subject to use in reply
+        encrypt_to_key: the key object to use for encrypting the email. (or
+            None)
+        gpgme_ctx: the gpgme context
+
+    Returns
+        A string version of the mime message, possibly encrypted and signed.
+    """
 
 
-    # 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)
+    plaintext_mime = MIMEText(plaintext)
+    plaintext_mime.set_charset('utf-8')
 
     if (encrypt_to_key != None):
 
 
     if (encrypt_to_key != None):
 
@@ -1047,7 +1383,10 @@ def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_ke
     else:
         message_mime = plaintext_mime
 
     else:
         message_mime = plaintext_mime
 
-    message_mime['To'] = email_from
+    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()
     message_mime['Subject'] = email_subject
 
     reply = message_mime.as_string()
@@ -1055,7 +1394,41 @@ def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_ke
     return reply
 
 
     return reply
 
 
+def send_reply(email_txt, reply_to, reply_from):
+    """Sends reply email
+
+    Sent to original sender
+
+    Args:
+        email_txt: message as a string
+        reply_to: recipient of reply
+        reply_from: edward's specific email address
+
+    Post:
+        Email is sent
+    """
+
+    if reply_to == None:
+        error("*** ERROR: No one to send email to.")
+        exit(1)
+
+    s = smtplib.SMTP('localhost')
+    s.sendmail(reply_from, reply_to, email_txt)
+    s.quit()
+
+
 def email_quote_text (text):
 def email_quote_text (text):
+    """Quotes input text by inserting "> "s
+
+    This is useful for quoting a text for the reply message. It inserts "> "
+    strings at the beginning of lines.
+
+    Args:
+        text: plain text to quote
+
+    Returns:
+        Quoted text
+    """
 
     quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
 
 
     quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
 
@@ -1063,7 +1436,20 @@ def email_quote_text (text):
 
 
 def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
 
 
 def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
+    """Encrypts and signs plaintext
+
+    This encrypts and signs a message.
+
+    Args:
+        plaintext: text to sign and ecrypt
+        encrypt_to_key: the key object to encrypt to
+        gpgme_ctx: the gpgme context
+
+    Returns:
+        An encrypted and signed string of text
+    """
 
 
+    # the plaintext should be mime encoded in an ascii-compatible form
     plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
     encrypted_bytes = io.BytesIO()
 
     plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
     encrypted_bytes = io.BytesIO()
 
@@ -1075,28 +1461,81 @@ def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
 
 
 def error (error_msg):
 
 
 def error (error_msg):
+    """Write an error message to stdout
+
+    The error message includes the program name.
+
+    Args:
+        error_msg: the message to print
+
+    Returns:
+        Nothing
+
+    Post:
+        An error message is printed to stdout
+    """
 
     sys.stderr.write(progname + ": " + str(error_msg) + "\n")
 
 
 def debug (debug_msg):
 
     sys.stderr.write(progname + ": " + str(error_msg) + "\n")
 
 
 def debug (debug_msg):
+    """Writes a debug message to stdout if debug == True
+
+    If the debug option is set in edward_config.py, then the passed message
+    gets printed to stdout.
+
+    Args:
+        debug_msg: the message to print to stdout
+
+    Returns:
+        Nothing
+
+    Post:
+        A debug message is printed to stdout
+    """
 
     if edward_config.debug == True:
         error(debug_msg)
 
 
 def handle_args ():
 
     if edward_config.debug == True:
         error(debug_msg)
 
 
 def handle_args ():
+    """Sets the progname variable and processes optional argument
+
+    If there are more than two arguments then edward complains and quits. An
+    single "-p" argument sets the print_reply_only option, which makes edward
+    print email replies instead of mailing them.
+
+    Args:
+        None
+
+    Returns:
+        True if edward should print arguments instead of mailing them,
+        otherwise it returns False.
+
+    Post:
+        Exits with error 1 if there are more than two arguments, otherwise
+        returns the print_reply_only option.
+    """
 
     global progname
     progname = sys.argv[0]
 
 
     global progname
     progname = sys.argv[0]
 
-    if len(sys.argv) > 1:
-        print(progname + ": error, this program doesn't " \
-                "need any arguments.", file=sys.stderr)
+    print_reply_only = False
+
+    if len(sys.argv) > 2:
+        print(progname + " usage:  " + progname + " [-p]\n\n" \
+                + "        -p      print reply message to stdout, do not mail it\n", \
+                file=sys.stderr)
         exit(1)
 
         exit(1)
 
+    elif (len(sys.argv) == 2) and (sys.argv[1] == "-p"):
+        print_reply_only = True
+
+    return print_reply_only
+
 
 if __name__ == "__main__":
 
 if __name__ == "__main__":
+    """Executes main if this file is not loaded interactively"""
 
     main()
 
 
     main()