updated my personal email address
[edward.git] / edward
diff --git a/edward b/edward
index 51278e9c0a9f1e7ec1339aa5127a7dcf793bb9bd..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,29 +86,27 @@ 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.
     """
 
     """
 
-    def __init__(self):
-        self.multipart          = False
-        self.subparts           = []
+    multipart               = False
+    subparts                = []
 
 
-        self.charset            = None
-        self.payload_bytes      = None
-        self.payload_pieces     = []
+    payload_bytes           = None
+    payload_pieces          = []
 
 
-        self.filename           = None
-        self.content_type       = None
-        self.description_list   = None
+    filename                = None
+    content_type            = None
+    content_disposition     = None
+    description_list        = None
 
 
 class PayloadPiece (object):
 
 
 class PayloadPiece (object):
@@ -110,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.
@@ -123,10 +129,9 @@ class PayloadPiece (object):
     on the contents of 'string'.
     """
 
     on the contents of 'string'.
     """
 
-    def __init__(self):
-        self.piece_type         = None
-        self.string             = None
-        self.gpg_data           = None
+    piece_type              = None
+    string                  = None
+    gpg_data                = None
 
 
 class GPGData (object):
 
 
 class GPGData (object):
@@ -143,15 +148,23 @@ 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.
     """
 
-    def __init__(self):
-        self.decrypted          = False
+    decrypted               = False
+
+    plainobj                = None
+    sigs                    = []
+    sigkey_missing          = False
+    key_cannot_encrypt      = False
+    keys                    = []
 
 
-        self.plainobj           = None
-        self.sigs               = []
-        self.keys               = []
 
 class ReplyInfo (object):
     """
 
 class ReplyInfo (object):
     """
@@ -172,43 +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.
     """
 
     """
 
-    def __init__(self):
-        self.replies                = None
+    replies                 = None
+
+    target_key              = None
+    fallback_target_key     = None
+    encrypt_to_key          = None
+    msg_to_quote            = ""
 
 
-        self.target_key             = None
-        self.fallback_target_key    = None
-        self.msg_to_quote           = ""
+    decrypt_success         = False
+    sig_success             = False
+    pubkey_success          = False
+    key_can_encrypt         = False
 
 
-        self.success_decrypt        = False
-        self.failed_decrypt         = False
-        self.public_key_received    = False
-        self.no_public_key          = False
-        self.sig_success            = False
-        self.sig_failure            = False
+    decrypt_failure         = False
+    sig_failure             = False
+    sigkey_missing          = False
+    key_cannot_encrypt      = False
+
+    have_reply_key          = False
 
 
 def main ():
 
 
 def main ():
@@ -223,7 +248,7 @@ def main ():
         None
 
     Returns:
         None
 
     Returns:
-        None
+        Nothing
 
     Pre:
         Mime or plaintext email passing in through standard input.  Portions of
 
     Pre:
         Mime or plaintext email passing in through standard input.  Portions of
@@ -240,32 +265,56 @@ 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):
+    """
+    This function returns the GPG context needed for encryption and signing.
+
+    The context is needed by other functions which use GPG functionality.
+
+    Args:
+        gnupghome: The path to "~/.gnupg/" or its alternative.
+        sign_with_key: The fingerprint of the key to sign with
+
+    Returns:
+        A gpgme context to be used for GPG functions.
+
+    Post:
+        the 'armor' flag is set to True and the list of signing keys contains
+        the single specified key
+    """
 
     os.environ['GNUPGHOME'] = gnupghome
 
 
     os.environ['GNUPGHOME'] = gnupghome
 
@@ -274,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)
@@ -284,9 +333,27 @@ 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
+    the EddyMsg class. It then splits each mime payload into one or more pieces
+    which may be plain text or GPG data. It then decrypts encrypted parts and
+    does some very basic signature verification on those parts.
 
 
-    email_struct = email.parser.Parser().parsestr(email_text)
+    Args:
+        email_bytes: an email message in byte string format
+        gpgme_ctx:  a gpgme context
+
+    Returns:
+        A message as an instance of EddyMsg
+
+    Post:
+        the returned EddyMsg instance has split, decrypted, verified and pubkey
+        imported payloads
+    """
+
+    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)
@@ -296,8 +363,20 @@ def parse_pgp_mime (email_text, gpgme_ctx):
 
 
 def parse_mime(msg_struct):
 
 
 def parse_mime(msg_struct):
+    """Translates python's email.parser format into an EddyMsg format
+
+    If the message is multi-part, then a recursive object is created, where
+    each sub-part is also a EddyMsg instance.
 
 
-    eddymsg_obj = EddyMsg()
+    Args:
+        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 = 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()
@@ -305,21 +384,34 @@ 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
+    PayloadPiece object. This function returns a list of these objects which
+    have been split into GPG data and regular text, if such splits need to be/
+    can be made.
+
+    Args:
+        payload_piece: a single payload or a split part of a payload
+        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:
+        a list of objects of the PayloadPiece class, in the order that the
+        string part of payload_piece originally was, broken up according to
+        matches specified by '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]
@@ -327,43 +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 mime part.
+
+    It copies needed data from an email.parser.BytesParser() object over to an
+    EddyMsg object.
+
+    Args:
+        part: an email.parser.BytesParser() object
+
+    Returns:
+        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:
@@ -373,6 +483,25 @@ def get_subpart_data (part):
 
 
 def do_to_eddys_pieces (function_to_do, eddymsg_obj, data):
 
 
 def do_to_eddys_pieces (function_to_do, eddymsg_obj, data):
+    """A function which maps another function onto a message's subparts.
+
+    This is a higer-order function which recursively performs a specified
+    function on each subpart of a multi-part message. Each single-part sub-part
+    has the function applied to it. This function also works if the part passed
+    in is single-part.
+
+    Args:
+        function_to_do: function to perform on sub-parts
+        eddymsg_obj: a single part or multi-part EddyMsg object
+        data: a second argument to pass to 'function_to_do'
+
+    Returns:
+        Nothing
+
+    Post:
+        The passed-in EddyMsg object is transformed recursively on its
+        sub-parts according to 'function_to_do'.
+    """
 
     if eddymsg_obj.multipart == True:
         for sub in eddymsg_obj.subparts:
 
     if eddymsg_obj.multipart == True:
         for sub in eddymsg_obj.subparts:
@@ -382,14 +511,54 @@ def do_to_eddys_pieces (function_to_do, eddymsg_obj, data):
 
 
 def split_payloads (eddymsg_obj):
 
 
 def split_payloads (eddymsg_obj):
+    """Splits all (sub-)payloads of a message into GPG data and regular text.
+
+    Recursively performs payload splitting on all sub-parts of an EddyMsg
+    object into the various GPG data types, such as GPG messages, public key
+    blocks and signed text.
+
+    Args:
+        eddymsg_obj: an instance of EddyMsg
+
+    Returns:
+        Nothing
+
+    Pre:
+        The EddyMsg object has payloads that are unsplit (by may be split)..
+
+    Post:
+        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.
 
 
-    (match_name, pattern) = match_type
+    This function splits up PayloadPiece objects into multipe PayloadPiece
+    objects and replaces the EddyMsg object's previous list of payload pieces
+    with the new split up one.
+
+    Args:
+        eddymsg_obj: a single-part EddyMsg object.
+        match_pair: a tuple from the match_pairs list, which specifies a match
+            name and a match pattern.
+
+    Returns:
+        Nothing
+
+    Pre:
+        The payload piece(s) of an EddyMsg object may be already split or
+        unsplit.
+
+    Post:
+        The EddyMsg object's payload piece(s) are split into a list of pieces
+        if matches of the match_pair are found.
+    """
+
+    (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:
@@ -399,6 +568,33 @@ def split_payload_pieces (eddymsg_obj, match_type):
 
 
 def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
 
 
 def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
+    """Performs GPG operations on the GPG parts of the message
+
+    This function decrypts text, verifies signatures, and imports public keys
+    included in an email.
+
+    Args:
+        eddymsg_obj: an EddyMsg object with its payload_pieces split into GPG
+            and non-GPG sections by split_payloads()
+        gpgme_ctx: a gpgme context
+
+        prev_parts: a list of mime parts that occur before the eddymsg_obj
+            part, under the same multi-part mime part. This is used for
+            verifying detached signatures. For the root mime part, this should
+            be an empty list, which is the default value if this paramater is
+            omitted.
+
+    Return:
+        Nothing
+
+    Pre:
+        eddymsg_obj should have its payloads split into gpg and non-gpg pieces.
+
+    Post:
+        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:
         prev_parts=[]
 
     if eddymsg_obj.multipart == True:
         prev_parts=[]
@@ -410,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)
+
+            piece.gpg_data.sigkey_missing = sigkey_missing
+            piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
 
 
-            if plaintext:
-                piece.piece_type = "signature"
-                piece.gpg_data = GPGData()
+            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 == "pubkey":
-            key_fps = add_gpg_key(piece.string, gpgme_ctx)
+        elif piece.piece_type == TxtType.pubkey:
+            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 != []:
 
             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
@@ -466,315 +666,695 @@ def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
 
 
 def prepare_for_reply (eddymsg_obj, replyinfo_obj):
 
 
 def prepare_for_reply (eddymsg_obj, replyinfo_obj):
+    """Updates replyinfo_obj with info on the message's GPG success/failures
+
+    This function marks replyinfo_obj with information about whether encrypted
+    text in eddymsg_obj was successfully decrypted, signatures were verified
+    and whether a public key was found or not.
+
+    Args:
+        eddymsg_obj: a message in the EddyMsg format
+        replyinfo_obj: an instance of ReplyInfo
+
+    Returns:
+        Nothing
+
+    Pre:
+        eddymsg_obj has had its gpg_data created by gpg_on_payloads
+
+    Post:
+        replyinfo_obj has been updated with info about decryption/sig
+        verififcation status, etc. However the desired key isn't imported until
+        later, so the success or failure of that updates the values set here.
+    """
 
     do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj)
 
 def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
 
     do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj)
 
 def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
+    """A helper function for prepare_for_reply
+
+    It updates replyinfo_obj with GPG success/failure information, when
+    supplied a single-part EddyMsg object.
+
+    Args:
+        eddymsg_obj: a single-part message in the EddyMsg format
+        replyinfo_obj: an object which holds information about the message's
+            GPG status
+
+    Returns:
+        Nothing
+
+    Pre:
+        eddymsg_obj is a single-part message. (it may be a part of a multi-part
+        message.) It has had its gpg_data created by gpg_on_payloads if it has
+        gpg data.
+
+    Post:
+        replyinfo_obj has been updated with gpg success/failure information
+    """
 
     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)
 
 
 def prepare_for_reply_message (piece, replyinfo_obj):
                     prepare_for_reply_sig(piece, replyinfo_obj)
 
 
 def prepare_for_reply_message (piece, replyinfo_obj):
+    """Helper function for prepare_for_reply()
 
 
-    if piece.gpg_data == None:
-        replyinfo_obj.failed_decrypt = True
+    This function is called when the piece_type of a payload piece is
+    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.
+
+    Returns:
+        Nothing
+
+    Pre:
+        the piece.payload_piece value should be TxtType.message.
+
+    Post:
+        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.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)
 
 
 def prepare_for_reply_pubkey (piece, replyinfo_obj):
 
     # to catch public keys in encrypted blocks
     prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
 
 
 def prepare_for_reply_pubkey (piece, replyinfo_obj):
+    """Helper function for prepare_for_reply(). Marks pubkey import status.
+
+    Marks replyinfo_obj with pub key import status.
+
+    Args:
+        piece: a PayloadPiece object
+        replyinfo_obj: a ReplyInfo object
 
 
-    if piece.gpg_data == None or piece.gpg_data.keys == []:
-        replyinfo_obj.no_public_key = True
+    Pre:
+        piece.piece_type should be set to TxtType.pubkey .
+
+    Post:
+        replyinfo_obj has its fields updated.
+    """
+
+    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):
+    """Helper function for prepare_for_reply(). Marks sig verification status.
+
+    Marks replyinfo_obj with signature verification status.
+
+    Args:
+        piece: a PayloadPiece object
+        replyinfo_obj: a ReplyInfo object
+
+    Pre:
+        piece.piece_type should be set to TxtType.signature, or
+        TxtType.detachedsig .
 
 
-    if piece.gpg_data == None or piece.gpg_data.sigs == []:
+    Post:
+        replyinfo_obj has its fields updated.
+    """
+
+    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, get_signed_part):
+def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part):
+    """For creating a string representation of a signed, encrypted part.
 
 
-    flat_string = ""
+    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
+
+    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.
+
+    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:
 
     for piece in eddymsg_obj.payload_pieces:
-        if piece.piece_type == "text":
-            flat_string += piece.string
-
         if (get_signed_part):
         if (get_signed_part):
-            # don't include nested encryption
-            if (piece.piece_type == "message") \
+            if ((piece.piece_type == TxtType.detachedsig) \
+                    or (piece.piece_type == TxtType.signature)) \
                     and (piece.gpg_data != None) \
                     and (piece.gpg_data != None) \
-                    and (piece.gpg_data.decrypted == False):
-                        flat_string += flatten_decrypted_payloads(piece.gpg_data.plainobj, get_signed_part)
+                    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:
+            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
 
 
-            elif ((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, get_signed_part)
 
 
-    return flat_string
+def get_key_from_fp (replyinfo_obj, gpgme_ctx):
+    """Obtains a public key object from a key fingerprint
 
 
+    If the .target_key is not set, then we use .fallback_target_key, if
+    available.
 
 
-def get_key_from_fp (replyinfo_obj, gpgme_ctx):
+    Args:
+        replyinfo_obj: ReplyInfo instance
+        gpgme_ctx: the gpgme context
 
 
-    if replyinfo_obj.target_key == None:
-        replyinfo_obj.target_key = replyinfo_obj.fallback_target_key
+    Return:
+        Nothing
 
 
-    if replyinfo_obj.target_key != None:
-        try:
-            encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key)
-            return encrypt_to_key
+    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.
 
 
-        except:
-            pass
+    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.
+    """
 
 
-    # no available key to use
-    replyinfo_obj.target_key = None
-    replyinfo_obj.fallback_target_key = None
+    for key in (replyinfo_obj.target_key, replyinfo_obj.fallback_target_key):
+        if key != None:
+            try:
+                encrypt_to_key = gpgme_ctx.get_key(key)
 
 
-    replyinfo_obj.no_public_key = True
-    replyinfo_obj.public_key_received = False
+            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
+
+            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
+    if replyinfo_obj.decrypt_success == True:
+        debug('decrypt success')
+        reply_plain += replyinfo_obj.replies['success_decrypt']
+        reply_plain += "\n\n"
 
 
-    elif replyinfo_obj.failed_decrypt == True:
+    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
 
 
-    plaintext = plain_b.getvalue().decode('utf-8')
+    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 = []
     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
 
 
-    return reply_key
+    Args:
+        key_obj: a gpgme key object
+
+    Returns:
+        A boolean representing key usability
+    """
+    if key_obj.can_encrypt and not key_obj.invalid and not key_obj.expired \
+            and not key_obj.revoked and not key_obj.disabled:
+        return True
+    else:
+        return False
+
+
+def test_auto_reply (email_bytes):
+    """Test whether email is auto-generated
+
+    If the email is autogenerated, edward quits without sending a response.
+    This is not a perfect test. Some auto-responses will go undetected.
+
+    Args:
+        email_bytes: the byte string from of the email
+
+    Returns:
+        Nothing, or exits the program
+    """
+
+    email_struct = email.parser.BytesHeaderParser().parsebytes(email_bytes)
+
+    auto_submitted = email_struct['Auto-Submitted']
+
+    if auto_submitted == None or auto_submitted == "no" \
+            or auto_submitted == "No":
+
+        return
 
 
+    debug("autoreply")
+    exit(0)
 
 
-def email_to_from_subject (email_text):
 
 
-    email_struct = email.parser.Parser().parsestr(email_text)
+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.
+
+    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.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.
 
 
+    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.
+    """
 
 
-def import_lang(email_to):
+    # 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
 
 
-                return language
+    lang_mod_name = "lang." + re.sub('-', '_', use_lang)
+    lang_module = importlib.import_module(lang_mod_name)
 
 
-    return importlib.import_module("lang.en")
+    reply_from = "edward-" + use_lang + "@" + hostname
 
 
+    return lang_module, reply_from
 
 
-def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
-                    gpgme_ctx):
 
 
-    # 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
+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.
 
 
-    # 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)
+    If the encrypt_key is included, then the email is encrypted and signed.
+    Otherwise it is unencrypted.
+
+    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.
+    """
+
+    plaintext_mime = MIMEText(plaintext)
+    plaintext_mime.set_charset('utf-8')
 
     if (encrypt_to_key != None):
 
 
     if (encrypt_to_key != None):
 
@@ -803,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()
@@ -811,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)
 
@@ -819,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()
 
@@ -831,27 +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 ():
-    if __name__ == "__main__":
+    """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]
+
+    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)
+
+    elif (len(sys.argv) == 2) and (sys.argv[1] == "-p"):
+        print_reply_only = True
 
 
-        global progname
-        progname = sys.argv[0]
+    return print_reply_only
 
 
-        if len(sys.argv) > 1:
-            print(progname + ": error, this program doesn't " \
-                    "need any arguments.", file=sys.stderr)
-            exit(1)
 
 
+if __name__ == "__main__":
+    """Executes main if this file is not loaded interactively"""
 
 
-main()
+    main()