updated my personal email address
[edward.git] / edward
diff --git a/edward b/edward
index 50b94fd7417d3b7d71924de17740144b0c323343..d7443927cabc24fa373c66b8019015be5d981dd2 100755 (executable)
--- a/edward
+++ b/edward
 
 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
 """
 
-import sys
-import gpgme
 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
 
+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
 
-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."""
 
+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-----'),
-                ('pubkey',
+                (TxtType.pubkey,
                 '-----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
@@ -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.
 
-    '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.
 
-    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
@@ -96,9 +103,9 @@ class EddyMsg (object):
     payload_bytes           = None
     payload_pieces          = []
 
-    charset                 = None
     filename                = None
     content_type            = None
+    content_disposition     = 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.
 
-    '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.
@@ -141,6 +148,12 @@ class GPGData (object):
 
     '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.
     """
 
@@ -148,6 +161,8 @@ class GPGData (object):
 
     plainobj                = None
     sigs                    = []
+    sigkey_missing          = False
+    key_cannot_encrypt      = False
     keys                    = []
 
 
@@ -170,42 +185,55 @@ class ReplyInfo (object):
     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.
 
-    '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.
 
-    '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
+    encrypt_to_key          = None
     msg_to_quote            = ""
 
-    success_decrypt         = False
-    failed_decrypt          = False
-    public_key_received     = False
-    no_public_key           = False
+    decrypt_success         = False
     sig_success             = False
+    pubkey_success          = False
+    key_can_encrypt         = False
+
+    decrypt_failure         = False
     sig_failure             = False
+    sigkey_missing          = False
+    key_cannot_encrypt      = False
+
+    have_reply_key          = False
 
 
 def main ():
@@ -237,29 +265,37 @@ def main ():
         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)
 
-    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)
-    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_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)
 
-    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):
@@ -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)
-    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)
@@ -297,7 +333,7 @@ def get_gpg_context (gnupghome, sign_with_key_fp):
     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
@@ -306,7 +342,7 @@ def parse_pgp_mime (email_text, gpgme_ctx):
     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:
@@ -317,7 +353,7 @@ def parse_pgp_mime (email_text, gpgme_ctx):
         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)
@@ -333,14 +369,14 @@ def parse_mime(msg_struct):
     each sub-part is also a EddyMsg instance.
 
     Args:
-        msg_struct: an email parsed with email.parser.Parser(), which can be
+        msg_struct: an email parsed with email.parser.BytesParser(), which can be
             multi-part
 
     Returns:
         an instance of EddyMsg, potentially a recursive one.
     """
 
-    eddymsg_obj = EddyMsg()
+    eddymsg_obj = get_subpart_data(msg_struct)
 
     if msg_struct.is_multipart() == True:
         payloads = msg_struct.get_payload()
@@ -348,13 +384,10 @@ def parse_mime(msg_struct):
         eddymsg_obj.multipart = True
         eddymsg_obj.subparts = list(map(parse_mime, payloads))
 
-    else:
-        eddymsg_obj = get_subpart_data(msg_struct)
-
     return eddymsg_obj
 
 
-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
@@ -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
-        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:
@@ -374,12 +407,11 @@ def scan_and_split (payload_piece, match_type, pattern):
     """
 
     # 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
-    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]
@@ -387,54 +419,61 @@ def scan_and_split (payload_piece, match_type, pattern):
     else:
 
         beginning               = PayloadPiece()
-        beginning.string        = matches.group('beginning')
+        beginning.string        = payload_piece.string[:matches.start()]
         beginning.piece_type    = payload_piece.piece_type
 
         match                   = PayloadPiece()
-        match.string            = matches.group('match')
-        match.piece_type        = match_type
+        match.string            = payload_piece.string[matches.start():matches.end()]
+        match.piece_type        = match_name
 
         rest                    = PayloadPiece()
-        rest.string             = matches.group('rest')
+        rest.string             = payload_piece.string[matches.end():]
         rest.piece_type         = payload_piece.piece_type
 
-        more_pieces = scan_and_split(rest, match_type, pattern)
+        more_pieces = scan_and_split(rest, match_name, pattern)
         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:
-        part: a non-multi-part mime.parser.Parser() object
+        part: an email.parser.BytesParser() object
 
     Returns:
-        a single-part EddyMsg() object
+        an EddyMsg() object
     """
 
     obj = EddyMsg()
 
-    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.content_disposition = part['content-disposition']
     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()
-            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:
@@ -491,11 +530,11 @@ def split_payloads (eddymsg_obj):
         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
@@ -504,7 +543,7 @@ def split_payload_pieces (eddymsg_obj, match_type):
 
     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:
@@ -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
-        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:
@@ -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:
-        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:
@@ -566,53 +606,57 @@ def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
 
     for piece in eddymsg_obj.payload_pieces:
 
-        if piece.piece_type == "text":
+        if piece.piece_type == TxtType.text:
             # 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.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.
-            (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.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 != []:
-                piece.gpg_data = GPGData()
                 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:
-                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 != []:
-                    piece.gpg_data = GPGData()
                     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:
-        if piece.piece_type == "text":
+        if piece.piece_type == TxtType.text:
             # 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)
 
-        elif piece.piece_type == "pubkey":
+        elif piece.piece_type == TxtType.pubkey:
             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)
 
 
@@ -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
-    "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.
 
-
     Returns:
         Nothing
 
     Pre:
-        the piece.payload_piece value should be "message".
+        the piece.payload_piece value should be TxtType.message.
 
     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
 
-    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
 
-    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
 
-    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)
@@ -743,19 +794,20 @@ def prepare_for_reply_pubkey (piece, replyinfo_obj):
         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.
     """
 
-    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:
-        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):
@@ -768,258 +820,541 @@ def prepare_for_reply_sig (piece, replyinfo_obj):
         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.
     """
 
-    if piece.gpg_data == None or piece.gpg_data.sigs == []:
+    if piece.gpg_data.sigs == []:
         replyinfo_obj.sig_failure = True
+
+        if piece.gpg_data.sigkey_missing == True:
+            replyinfo_obj.sigkey_missing = True
+
+        if piece.gpg_data.key_cannot_encrypt == True:
+            replyinfo_obj.key_cannot_encrypt = True
+
     else:
         replyinfo_obj.sig_success = True
 
         if replyinfo_obj.fallback_target_key == None:
             replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
 
+    if (piece.piece_type == TxtType.signature):
+        # to catch public keys in signature blocks
+        prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
+
+
+def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part):
+    """For creating a string representation of a signed, encrypted part.
 
+    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:
-        return ""
+        return
 
     # 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):
-            # don't include nested encryption
-            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:
-            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):
+    """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):
+    """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 = ""
 
-    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 += "\n\n"
 
 
     if replyinfo_obj.sig_success == True:
-        reply_plain += "\n\n"
+        debug('signature success')
         reply_plain += replyinfo_obj.replies['sig_success']
+        reply_plain += "\n\n"
 
     elif replyinfo_obj.sig_failure == True:
-        reply_plain += "\n\n"
+        debug('signature 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 += "\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"
+
+    elif (replyinfo_obj.key_can_encrypt == False) \
+            and (replyinfo_obj.key_cannot_encrypt == True):
+        debug('bad public key')
         reply_plain += replyinfo_obj.replies['no_public_key']
+        reply_plain += "\n\n"
+
+
+    if (replyinfo_obj.decrypt_success == True) \
+            and (replyinfo_obj.sig_success == True) \
+            and (replyinfo_obj.have_reply_key == True):
+        debug('message quoted')
+        reply_plain += replyinfo_obj.replies['quote_follows']
+        reply_plain += "\n\n"
+        quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
+        reply_plain += quoted_text
+        reply_plain += "\n\n"
+
+
+    if (reply_plain == ""):
+        debug('plaintext message')
+        reply_plain += replyinfo_obj.replies['failed_decrypt']
+        reply_plain += "\n\n"
 
 
-    reply_plain += "\n\n"
     reply_plain += replyinfo_obj.replies['signature']
+    reply_plain += "\n\n"
 
     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'))
 
-    result = gpgme_ctx.import_(fp)
-    imports = result.imports
+    try:
+        result = gpgme_ctx.import_(fp)
+        imports = result.imports
+    except gpgme.GpgmeError:
+        imports = []
 
     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_cannot_encrypt = False
 
             debug("added gpg key: " + fingerprint)
 
-    return key_fingerprints
+        elif key_fingerprints == []:
+            key_cannot_encrypt = True
+
+    return (key_fingerprints, key_cannot_encrypt)
 
 
 def verify_sig_message (msg_block, gpgme_ctx):
+    """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)
-    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)
-    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):
+    """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)
-    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 = []
+
     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_reply_to  = email_struct['Reply-To']
+
     email_subject   = email_struct['Subject']
 
-    return email_to, email_from, email_subject
+    if email_reply_to == None:
+        email_reply_to = email_from
+
+    return email_to, email_reply_to, email_subject
+
+
+def import_lang_pick_address(email_to, hostname):
+    """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:
-                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):
 
@@ -1048,7 +1383,10 @@ def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_ke
     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()
@@ -1056,7 +1394,41 @@ def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_ke
     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):
+    """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)
 
@@ -1064,7 +1436,20 @@ def email_quote_text (text):
 
 
 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()
 
@@ -1076,28 +1461,81 @@ def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
 
 
 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):
+    """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 ():
+    """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]
 
-    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)
 
+    elif (len(sys.argv) == 2) and (sys.argv[1] == "-p"):
+        print_reply_only = True
+
+    return print_reply_only
+
 
 if __name__ == "__main__":
+    """Executes main if this file is not loaded interactively"""
 
     main()