catch semi-specific errors only.
[edward.git] / edward
diff --git a/edward b/edward
index ca6ef082affc034fe44be65fb9bb19df0b9b5122..1dad2e1c8062dc2aab55e7c658f9304598c2e838 100755 (executable)
--- a/edward
+++ b/edward
@@ -29,7 +29,6 @@ Code sourced from these projects:
   * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/CURRENT/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
@@ -37,6 +36,7 @@ import gpgme
 import re
 import io
 import os
+import importlib
 
 import email.parser
 import email.message
@@ -48,268 +48,1156 @@ from email.mime.nonmultipart    import MIMENonMultipart
 
 import edward_config
 
+langs = ["de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
+
+"""This list contains the abbreviated names of reply languages available to
+edward."""
+
+
+match_types =  [('message',
+                '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
+                ('pubkey',
+                '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
+                ('detachedsig',
+                '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
+
+"""This list of tuples matches query names with re.search() queries used
+to find GPG data for edward to process."""
+
+
+class EddyMsg (object):
+    """
+    The EddyMsg class represents relevant parts of a mime message.
+
+    The represented message can be single-part or multi-part.
+
+    'multipart' is set to True if there are multiple mime parts.
+
+    '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' 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 'filename', 'content_type' and 'description_list' come from the mime
+    part parameters.
+    """
+
+    multipart               = False
+    subparts                = []
+
+    payload_bytes           = None
+    payload_pieces          = []
+
+    filename                = None
+    content_type            = None
+    description_list        = None
+
+
+class PayloadPiece (object):
+    """
+    PayloadPiece represents a complte or sub-section of a mime part.
+
+    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.
+
+    'string' contains some string of text, such as non-GPG text, an encrypted
+    block of text, a signature, or a public key.
+
+    'gpg_data' points to any instances of GPGData that have been created based
+    on the contents of 'string'.
+    """
+
+    piece_type              = None
+    string                  = None
+    gpg_data                = None
+
+
+class GPGData (object):
+    """
+    GPGData holds info from decryption, sig. verification, and/or pub. keys.
+
+    Instances of this class contain decrypted information, signature
+    fingerprints and/or fingerprints of processed and imported public keys.
+
+    'decrypted' is set to True if 'plainobj' was created from encrypted data.
+
+    'plainobj' points to any decrypted, or signed part of, a GPG signature. It
+    is intended to be an instance of the EddyMsg class.
+
+    'sigs' is a list of fingerprints of keys used to sign the data in plainobj.
+
+    'keys' is a list of fingerprints of keys obtained in public key blocks.
+    """
+
+    decrypted               = False
+
+    plainobj                = None
+    sigs                    = []
+    keys                    = []
+
+
+class ReplyInfo (object):
+    """
+    ReplyInfo contains details that edward uses in generating its reply.
+
+    Instances of this class contain information about whether a message was
+    successfully encrypted or signed, and whether a public key was attached, or
+    retrievable, from the local GPG store. It stores the fingerprints of
+    potential encryption key candidates and the message (if any at all) to
+    quote in edward's reply.
+
+    'replies' points one of the dictionaries of translated replies.
+
+    'target_key' refers to the fingerprint of a key used to sign encrypted
+    data. This is the preferred key, if it is set, and if is available.
+
+    'fallback_target_key' referst to the fingerprint of a key used to sign
+    unencrypted data; alternatively it may be a public key attached to the
+    message.
+
+    '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
+    message.
+
+    'failed_decrypt' is set to True if edward failed to decrypt part of the
+    message.
+
+    'publick_key_received' is set to True if edward successfully imported a
+    public key.
+
+    'no_public_key' is set to True if edward doesn't have a key to encrypt to
+    when replying to the user.
+
+    'sig_success' is set to True if edward could to some extent verify the
+    signature of a signed part of the message to edward.
+
+    'sig_failure' is set to True if edward failed to some extent verify the
+    signature of a signed part of the message to edward.
+    """
+
+    replies                 = None
+
+    target_key              = None
+    fallback_target_key     = None
+    msg_to_quote            = ""
+
+    success_decrypt         = False
+    failed_decrypt          = False
+    public_key_received     = False
+    no_public_key           = False
+    sig_success             = False
+    sig_failure             = False
+
+
 def main ():
 
+    """
+    This is the main function for edward, a GPG reply bot.
+
+    Edward responds to GPG-encrypted and signed mail, encrypting and signing
+    the response if the user's public key is, or was, included in the message.
+
+    Args:
+        None
+
+    Returns:
+        Nothing
+
+    Pre:
+        Mime or plaintext email passing in through standard input.  Portions of
+        the email may be encrypted. If the To: address contains the text
+        "edward-ja", then the reply will contain a reply written in the
+        Japanese language.  There are other languages as well. The default
+        language is English.
+
+    Post:
+        A reply email will be printed to standard output. The contents of the
+        reply email depends on whether the original email was encrypted or not,
+        has or doesn't have a signature, whether a public key used in the
+        original message is provided or locally stored, and the language
+        implied by the To: address in the original email.
+    """
+
     handle_args()
 
+    gpgme_ctx = get_gpg_context(edward_config.gnupghome,
+                              edward_config.sign_with_key)
+
     email_text = sys.stdin.read()
-    email_from, email_subject = email_from_subject(email_text)
+    email_struct = parse_pgp_mime(email_text, gpgme_ctx)
+
+    email_to, email_from, email_subject = email_to_from_subject(email_text)
+    lang = import_lang(email_to)
+
+    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)
+    reply_plaintext = write_reply(replyinfo_obj)
+
+    reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \
+                                         email_subject, encrypt_to_key,
+                                         gpgme_ctx)
+
+    print(reply_mime)
+
+
+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'] = edward_config.gnupghome
     gpgme_ctx = gpgme.Context()
     gpgme_ctx.armor = True
 
-    add_gpg_keys(email_text, gpgme_ctx)
+    try:
+        sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
+    except gpgme.GpgmeError:
+        error("unable to load signing key. is the gnupghome "
+                + "and signing key properly set in the edward_config.py?")
+        exit(1)
+
+    gpgme_ctx.signers = [sign_with_key]
+
+    return gpgme_ctx
 
-    plaintext, keys = email_decode_flatten(email_text, gpgme_ctx)
-    encrypt_to_key = choose_reply_encryption_key(keys)
 
-    reply_message = generate_reply(plaintext, email_from, \
-                                   email_subject, encrypt_to_key,
-                                   edward_config.sign_with_key,
-                                   gpgme_ctx)
+def parse_pgp_mime (email_text, gpgme_ctx):
+    """Parses the email for mime payloads and decrypts/verfies signatures.
 
-    print(reply_message)
+    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.
 
+    Args:
+        email_text: an email message in string format
+        gpgme_ctx:  a gpgme context
 
-def email_decode_flatten (email_text, gpgme_ctx):
+    Returns:
+        A message as an instance of EddyMsg
 
-    body = ""
-    keys = []
+    Post:
+        the returned EddyMsg instance has split, decrypted, verified and pubkey
+        imported payloads
+    """
 
     email_struct = email.parser.Parser().parsestr(email_text)
 
-    for subpart in email_struct.walk():
+    eddymsg_obj = parse_mime(email_struct)
+    split_payloads(eddymsg_obj)
+    gpg_on_payloads(eddymsg_obj, gpgme_ctx)
 
-        payload, description, filename, content_type \
-                = get_email_subpart_info(subpart)
+    return eddymsg_obj
 
-        if payload == "":
-            continue
 
-        if content_type == "multipart":
-            continue
+def parse_mime(msg_struct):
+    """Translates python's email.parser format into an EddyMsg format
 
-        if content_type == "application/pgp-encrypted":
-            if description == "PGP/MIME version identification":
-                if payload.strip() != "Version: 1":
-                    print(progname + ": Warning: unknown " \
-                            + description + ": " \
-                            + payload.strip(), file=sys.stderr)
-            continue
+    If the message is multi-part, then a recursive object is created, where
+    each sub-part is also a EddyMsg instance.
 
+    Args:
+        msg_struct: an email parsed with email.parser.Parser(), which can be
+            multi-part
 
-        if (filename == "encrypted.asc") or (content_type == "pgp/mime"):
-            plaintext, more_keys = decrypt_text(payload, gpgme_ctx)
+    Returns:
+        an instance of EddyMsg, potentially a recursive one.
+    """
 
-            body += plaintext
-            keys += more_keys
+    eddymsg_obj = EddyMsg()
 
-        elif content_type == "application/pgp-keys":
-            keys += add_gpg_keys(payload, gpgme_ctx)
+    if msg_struct.is_multipart() == True:
+        payloads = msg_struct.get_payload()
 
-        elif content_type == "text/plain":
-            body += payload + "\n"
+        eddymsg_obj.multipart = True
+        eddymsg_obj.subparts = list(map(parse_mime, payloads))
 
-        else:
-            body += payload + "\n"
+    else:
+        eddymsg_obj = get_subpart_data(msg_struct)
 
-    return body, keys
+    return eddymsg_obj
 
 
-def email_from_subject (email_text):
+def scan_and_split (payload_piece, match_type, pattern):
+    """This splits the payloads of an EddyMsg object into GPG and text parts.
 
-    email_struct = email.parser.Parser().parsestr(email_text)
+    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_type: 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
+    if payload_piece.piece_type != "text":
+        return [payload_piece]
+
+    flags = re.DOTALL | re.MULTILINE
+    matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
+                        ")(?P<rest>.*)", payload_piece.string, flags=flags)
+
+    if matches == None:
+        pieces = [payload_piece]
+
+    else:
+
+        beginning               = PayloadPiece()
+        beginning.string        = matches.group('beginning')
+        beginning.piece_type    = payload_piece.piece_type
+
+        match                   = PayloadPiece()
+        match.string            = matches.group('match')
+        match.piece_type        = match_type
+
+        rest                    = PayloadPiece()
+        rest.string             = matches.group('rest')
+        rest.piece_type         = payload_piece.piece_type
+
+        more_pieces = scan_and_split(rest, match_type, pattern)
+        pieces = [beginning, match ] + more_pieces
+
+    return pieces
 
-    email_from      = email_struct['From']
-    email_subject   = email_struct['Subject']
 
-    return email_from, email_subject
+def get_subpart_data (part):
+    """This function grabs information from a single part mime object.
 
+    It copies needed data from a single part email.parser.Parser() object over
+    to an EddyMsg object.
 
-def get_email_subpart_info (part):
+    Args:
+        part: a non-multi-part mime.parser.Parser() object
 
-    charset             = part.get_content_charset()
-    payload_bytes       = part.get_payload(decode=True)
+    Returns:
+        a single-part EddyMsg() object
+    """
 
-    filename            = part.get_filename()
-    content_type        = part.get_content_type()
-    description_list    = part.get_params(header='content-description')
+    charset                 = part.get_content_charset()
+    mime_decoded_bytes      = part.get_payload(decode=True)
 
+    obj = EddyMsg()
+    obj.payload_bytes       = part.as_bytes()
+
+    obj.filename            = part.get_filename()
+    obj.content_type        = part.get_content_type()
+    obj.description_list    = part['content-description']
+
+    # your guess is as good as a-myy-ee-ine...
     if charset == None:
         charset = 'utf-8'
 
-    if payload_bytes != None:
-        payload = payload_bytes.decode(charset)
+    if mime_decoded_bytes != None:
+        try:
+            payload = PayloadPiece()
+            payload.string = mime_decoded_bytes.decode(charset)
+            payload.piece_type = 'text'
+
+            obj.payload_pieces = [payload]
+        except UnicodeDecodeError:
+            pass
+
+    return obj
+
+
+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:
+            do_to_eddys_pieces(function_to_do, sub, data)
+    else:
+        function_to_do(eddymsg_obj, data)
+
+
+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)
+
+
+def split_payload_pieces (eddymsg_obj, match_type):
+    """A helper function for split_payloads(); works on PayloadPiece objects.
+
+    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_type: a tuple from the match_types 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_type are found.
+    """
+
+    (match_name, pattern) = match_type
+
+    new_pieces_list = []
+    for piece in eddymsg_obj.payload_pieces:
+        new_pieces_list += scan_and_split(piece, match_name, pattern)
+
+    eddymsg_obj.payload_pieces = new_pieces_list
+
+
+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 member of
+        PayloadPiece objects get filled in with GPGData objects.
+    """
+
+    if eddymsg_obj.multipart == True:
+        prev_parts=[]
+        for sub in eddymsg_obj.subparts:
+            gpg_on_payloads (sub, gpgme_ctx, prev_parts)
+            prev_parts += [sub]
+
+        return
+
+    for piece in eddymsg_obj.payload_pieces:
+
+        if piece.piece_type == "text":
+            # don't transform the plaintext.
+            pass
+
+        elif piece.piece_type == "message":
+            (plaintext, sigs) = decrypt_block(piece.string, gpgme_ctx)
+
+            if plaintext:
+                piece.gpg_data = GPGData()
+                piece.gpg_data.decrypted = True
+                piece.gpg_data.sigs = sigs
+                # recurse!
+                piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
+                continue
+
+            # if not encrypted, check to see if this is an armored signature.
+            (plaintext, sigs) = verify_sig_message(piece.string, gpgme_ctx)
+
+            if plaintext:
+                piece.piece_type = "signature"
+                piece.gpg_data = GPGData()
+                piece.gpg_data.sigs = sigs
+                # recurse!
+                piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
+
+        # FIXME: handle pubkeys first, so that signatures can be validated
+        # on freshly imported keys
+        elif piece.piece_type == "pubkey":
+            key_fps = add_gpg_key(piece.string, gpgme_ctx)
+
+            if key_fps != []:
+                piece.gpg_data = GPGData()
+                piece.gpg_data.keys = key_fps
+
+        elif piece.piece_type == "detachedsig":
+            for prev in prev_parts:
+                sig_fps = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx)
+
+                if sig_fps != []:
+                    piece.gpg_data = GPGData()
+                    piece.gpg_data.sigs = sig_fps
+                    piece.gpg_data.plainobj = prev
+                    break
+
+        else:
+            pass
+
+
+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):
+    """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:
+        if piece.piece_type == "text":
+            # don't quote the plaintext part.
+            pass
+
+        elif piece.piece_type == "message":
+            prepare_for_reply_message(piece, replyinfo_obj)
+
+        elif piece.piece_type == "pubkey":
+            prepare_for_reply_pubkey(piece, replyinfo_obj)
+
+        elif (piece.piece_type == "detachedsig") \
+            or (piece.piece_type == "signature"):
+                    prepare_for_reply_sig(piece, replyinfo_obj)
+
+
+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.
+
+    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".
+
+    Post:
+        replyinfo_obj gets updated with decryption status, signing status and a
+        potential signing key.
+    """
+
+    if piece.gpg_data == None:
+        replyinfo_obj.failed_decrypt = True
+        return
+
+    replyinfo_obj.success_decrypt = 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:
+        # only include a signed message in the reply.
+        get_signed_part = True
+
+    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):
+    """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
+
+    Pre:
+        piece.piece_type should be set to "pubkey".
+
+    Post:
+        replyinfo_obj has its fields updated.
+    """
+
+    if piece.gpg_data == None or piece.gpg_data.keys == []:
+        replyinfo_obj.no_public_key = True
     else:
-        payload = ""
+        replyinfo_obj.public_key_received = True
+
+        if replyinfo_obj.fallback_target_key == None:
+            replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
+
+
+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 "signature", or "detachedsig".
+
+    Post:
+        replyinfo_obj has its fields updated.
+    """
 
-    if description_list != None:
-        description = description_list[0][0]
+    if piece.gpg_data == None or piece.gpg_data.sigs == []:
+        replyinfo_obj.sig_failure = True
     else:
-        description = ""
+        replyinfo_obj.sig_success = True
+
+        if replyinfo_obj.fallback_target_key == None:
+            replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
+
+
+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
+
+    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:
+        return
+
+    # recurse on multi-part mime
+    if eddymsg_obj.multipart == True:
+        for sub in eddymsg_obj.subparts:
+            flatten_decrypted_payloads(sub, replyinfo_obj, get_signed_part)
+
+    for piece in eddymsg_obj.payload_pieces:
+        if (get_signed_part):
+            if ((piece.piece_type == "detachedsig") \
+                    or (piece.piece_type == "signature")) \
+                    and (piece.gpg_data != 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":
+                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 the .target_key is not set, then we use .fallback_target_key.
+
+    Args:
+        replyinfo_obj: ReplyInfo instance
+        gpgme_ctx: the gpgme context
+
+    Return:
+        The key object of the key of either the target_key or the fallback one
+        if .target_key is not set. If the key cannot be loaded, then return
+        None.
+
+    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.
+
+    Post:
+        If the key cannot be loaded, then the replyinfo_obj is marked for
+        having no public key available.
+    """
+
+    if replyinfo_obj.target_key == None:
+        replyinfo_obj.target_key = replyinfo_obj.fallback_target_key
+
+    if replyinfo_obj.target_key != None:
+        try:
+            encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key)
+            return encrypt_to_key
+
+        except gpgme.GpgmeError:
+            pass
+
+    # no available key to use
+    replyinfo_obj.target_key = None
+    replyinfo_obj.fallback_target_key = None
+
+    replyinfo_obj.no_public_key = True
+    replyinfo_obj.public_key_received = False
+
+    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.no_public_key == False:
+            quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
+            reply_plain += quoted_text
+
+    elif replyinfo_obj.failed_decrypt == True:
+        reply_plain += replyinfo_obj.replies['failed_decrypt']
+
+
+    if replyinfo_obj.sig_success == True:
+        reply_plain += "\n\n"
+        reply_plain += replyinfo_obj.replies['sig_success']
+
+    elif replyinfo_obj.sig_failure == True:
+        reply_plain += "\n\n"
+        reply_plain += replyinfo_obj.replies['sig_failure']
 
-    return payload, description, filename, content_type
 
+    if replyinfo_obj.public_key_received == True:
+        reply_plain += "\n\n"
+        reply_plain += replyinfo_obj.replies['public_key_received']
 
-def add_gpg_keys (text, gpgme_ctx):
+    elif replyinfo_obj.no_public_key == True:
+        reply_plain += "\n\n"
+        reply_plain += replyinfo_obj.replies['no_public_key']
 
-    key_blocks = scan_and_grab(text,
-                               '-----BEGIN PGP PUBLIC KEY BLOCK-----',
-                               '-----END PGP PUBLIC KEY BLOCK-----')
 
-    keys = []
-    for key_block in key_blocks:
-        fp = io.BytesIO(key_block.encode('ascii'))
+    reply_plain += "\n\n"
+    reply_plain += replyinfo_obj.replies['signature']
 
-        result = gpgme_ctx.import_(fp)
-        imports = result.imports
+    return reply_plain
 
-        if imports != []:
-            fingerprint = imports[0][0]
-            keys += [gpgme_ctx.get_key(fingerprint)]
+
+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)
+    """
+
+    fp = io.BytesIO(key_block.encode('ascii'))
+
+    result = gpgme_ctx.import_(fp)
+    imports = result.imports
+
+    key_fingerprints = []
+
+    if imports != []:
+        for import_ in imports:
+            fingerprint = import_[0]
+            key_fingerprints += [fingerprint]
 
             debug("added gpg key: " + fingerprint)
 
-    return keys
+    return key_fingerprints
 
 
-def decrypt_text (gpg_text, gpgme_ctx):
+def verify_sig_message (msg_block, gpgme_ctx):
+    """Verifies the signature of a signed, ascii-armored block of text.
 
-    body = ""
-    keys = []
+    It encodes the string into ascii, since binary GPG files are currently
+    unsupported, and alternative, the ascii-armored format is encodable into
+    ascii.
 
-    msg_blocks = scan_and_grab(gpg_text,
-                               '-----BEGIN PGP MESSAGE-----',
-                               '-----END PGP MESSAGE-----')
+    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
 
-    plaintexts_and_sigs = decrypt_blocks(msg_blocks, gpgme_ctx)
+    Returns:
+        A tuple of the plaintext of the signed part and the list of
+        fingerprints of keys signing the data. If verification failed, perhaps
+        because the message was also encrypted, then empty results are
+        returned.
+    """
 
-    for pair in plaintexts_and_sigs:
-        plaintext   = pair[0]
-        sigs        = pair[1]
+    block_b = io.BytesIO(msg_block.encode('ascii'))
+    plain_b = io.BytesIO()
 
-        for sig in sigs:
-            keys += [gpgme_ctx.get_key(sig.fpr)]
+    try:
+        sigs = gpgme_ctx.verify(block_b, None, plain_b)
+    except gpgme.GpgmeError:
+        return ("",[])
 
-        # recursive for nested layers of mime and/or gpg
-        plaintext, more_keys = email_decode_flatten(plaintext, gpgme_ctx)
+    plaintext = plain_b.getvalue().decode('utf-8')
 
-        body += plaintext
-        keys += more_keys
+    fingerprints = []
+    for sig in sigs:
+        fingerprints += [sig.fpr]
+    return (plaintext, fingerprints)
 
-    return body, keys
 
+def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
+    """Verifies the signature of a detached signature.
 
-def scan_and_grab (text, start_text, end_text):
+    This requires the signature part and the signed part as separate arguments.
 
-    matches = re.search('(' + start_text + '.*' + end_text + ')',
-                        text, flags=re.DOTALL)
+    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
 
-    if matches != None:
-        match_tuple = matches.groups()
-    else:
-        match_tuple = ()
+    Returns:
+        A list of signing fingerprints if the signature verification was
+        sucessful. Otherwise, an empty list is returned.
+    """
 
-    return match_tuple
+    detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
+    plaintext_fp = io.BytesIO(plaintext_bytes)
 
+    try:
+        result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
+    except gpgme.GpgmeError:
+        return []
 
-def decrypt_blocks (msg_blocks, gpgme_ctx):
+    sig_fingerprints = []
+    for res_ in result:
+        sig_fingerprints += [res_.fpr]
 
-    return [decrypt_block(block, gpgme_ctx) for block in msg_blocks]
+    return sig_fingerprints
 
 
 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 of plaintext and signatures, if the decryption and signature
+        verification were successful, respectively.
+    """
 
     block_b = io.BytesIO(msg_block.encode('ascii'))
     plain_b = io.BytesIO()
 
     try:
         sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
-    except:
+    except gpgme.GpgmeError:
         return ("",[])
 
     plaintext = plain_b.getvalue().decode('utf-8')
-    return (plaintext, sigs)
 
+    fingerprints = []
+    for sig in sigs:
+        fingerprints += [sig.fpr]
+    return (plaintext, fingerprints)
 
-def choose_reply_encryption_key (keys):
 
-    reply_key = None
-    for key in keys:
-        if (key.can_encrypt == True):
-            reply_key = key
-            break
+def email_to_from_subject (email_text):
+    """Returns the values of the email's To:, From: and Subject: fields
 
-    return reply_key
+    Returns this information from an email.
 
+    Args:
+        email_text: the string form of the email
 
-def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
-                    sign_with_fingerprint, gpgme_ctx):
+    Returns:
+        the email To:, From:, and Subject: fields as strings
+    """
 
+    email_struct = email.parser.Parser().parsestr(email_text)
 
-    reply  = "To: " + email_from + "\n"
-    reply += "Subject: " + email_subject + "\n"
+    email_to        = email_struct['To']
+    email_from      = email_struct['From']
+    email_subject   = email_struct['Subject']
 
-    if (encrypt_to_key != None):
-        plaintext_reply  = "thanks for the message!\n\n\n"
-        plaintext_reply += email_quote_text(plaintext)
+    return email_to, email_from, email_subject
+
+
+def import_lang(email_to):
+    """Imports appropriate language file for basic i18n support
+
+    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.
+
+    Args:
+        email_to: the string containing the email address that the mail was
+        sent to.
+
+    Returns:
+        the reference to the imported language module. The only variable in
+        this file is the 'replies' dictionary.
+    """
+
+    if email_to != None:
+        for lang in langs:
+            if "edward-" + lang in email_to:
+                lang = "lang." + re.sub('-', '_', lang)
+                language = importlib.import_module(lang)
+
+                return language
+
+    return importlib.import_module("lang.en")
+
+
+def generate_encrypted_mime (plaintext, email_to, email_subject, encrypt_to_key,
+                    gpgme_ctx):
+    """This function creates the mime email reply. It can encrypt the email.
+
+    If the encrypt_key is included, then the email is encrypted and signed.
+    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.
+    """
+
+    # 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
 
-        # quoted printable encoding lets most ascii characters look normal
-        # before the decrypted mime message is decoded.
-        char_set = email.charset.Charset("utf-8")
-        char_set.body_encoding = email.charset.QP
+    # 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)
 
-        # MIMEText doesn't allow setting the text encoding
-        # so we use MIMENonMultipart.
-        plaintext_mime = MIMENonMultipart('text', 'plain')
-        plaintext_mime.set_payload(plaintext_reply, charset=char_set)
+    if (encrypt_to_key != None):
 
         encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
                                               encrypt_to_key,
-                                              sign_with_fingerprint,
                                               gpgme_ctx)
+        gpg_payload = encrypted_text
 
-        control_mime = MIMEApplication("Version: 1",
-                                       _subtype='pgp-encrypted',
-                                       _encoder=email.encoders.encode_7or8bit)
-        control_mime['Content-Description'] = 'PGP/MIME version identification'
-        control_mime.set_charset('us-ascii')
+    else:
+        signed_text = sign_message(plaintext_mime.as_string(), gpgme_ctx)
+        gpg_payload = signed_text
 
-        encoded_mime = MIMEApplication(encrypted_text,
-                                       _subtype='octet-stream; name="encrypted.asc"',
-                                       _encoder=email.encoders.encode_7or8bit)
-        encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
-        encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
-        encoded_mime.set_charset('us-ascii')
+    control_mime = MIMEApplication("Version: 1",
+                                   _subtype='pgp-encrypted',
+                                   _encoder=email.encoders.encode_7or8bit)
+    control_mime['Content-Description'] = 'PGP/MIME version identification'
+    control_mime.set_charset('us-ascii')
 
-        message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
-        message_mime.attach(control_mime)
-        message_mime.attach(encoded_mime)
-        message_mime['Content-Disposition'] = 'inline'
+    encoded_mime = MIMEApplication(gpg_payload,
+                                   _subtype='octet-stream; name="encrypted.asc"',
+                                   _encoder=email.encoders.encode_7or8bit)
+    encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
+    encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
+    encoded_mime.set_charset('us-ascii')
 
-        reply += message_mime.as_string()
+    message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
+    message_mime.attach(control_mime)
+    message_mime.attach(encoded_mime)
+    message_mime['Content-Disposition'] = 'inline'
 
-    else:
-        reply += "\n"
-        reply += "Sorry, i couldn't find your key.\n"
-        reply += "I'll need that to encrypt a message to you."
+
+    message_mime['To'] = email_to
+    message_mime['Subject'] = email_subject
+
+    reply = message_mime.as_string()
 
     return reply
 
 
 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)
 
     return quoted_message
 
 
-def encrypt_sign_message (plaintext, encrypt_to_key, sign_with_fingerprint, gpgme_ctx):
+def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
+    """Encrypts and signs plaintext
 
-    sign_with_key = gpgme_ctx.get_key(sign_with_fingerprint)
-    gpgme_ctx.signers = [sign_with_key]
+    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()
 
@@ -320,23 +1208,95 @@ def encrypt_sign_message (plaintext, encrypt_to_key, sign_with_fingerprint, gpgm
     return encrypted_txt
 
 
+def sign_message (plaintext, gpgme_ctx):
+    """Signs plaintext
+
+    This signs a message.
+
+    Args:
+        plaintext: text to sign
+        gpgme_ctx: the gpgme context
+
+    Returns:
+        An armored signature as a string of text
+    """
+
+    # the plaintext should be mime encoded in an ascii-compatible form
+    plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
+    signed_bytes = io.BytesIO()
+
+    gpgme_ctx.sign(plaintext_bytes, signed_bytes, gpgme.SIG_MODE_NORMAL)
+
+    signed_txt = signed_bytes.getvalue().decode('ascii')
+    return signed_txt
+
+
+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:
-        sys.stderr.write(debug_msg + "\n")
+        error(debug_msg)
 
 
 def handle_args ():
-    if __name__ == "__main__":
+    """Sets the progname variable and complains about any arguments
+
+    If there are any arguments, then edward complains and quits, because input
+    is read from stdin.
+
+    Args:
+        None
+
+    Returns:
+        None
+
+    Post:
+        Exits with error 1 if there are arguments, otherwise returns to the
+        calling function, such as main().
+    """
+
+    global progname
+    progname = sys.argv[0]
 
-        global progname
-        progname = sys.argv[0]
+    if len(sys.argv) > 1:
+        print(progname + ": error, this program doesn't " \
+                "need any arguments.", file=sys.stderr)
+        exit(1)
 
-        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()