updated my personal email address
[edward.git] / edward
diff --git a/edward b/edward
index a7c7874549663263ff68d8f3120add81ee2e6ad5..d7443927cabc24fa373c66b8019015be5d981dd2 100755 (executable)
--- a/edward
+++ b/edward
 
 Code sourced from these projects:
 
 
 Code sourced from these projects:
 
-  * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/CURRENT/edward.tar.gz
+  * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/PREVIOUS-20150530/edward.tar.gz
   * https://git-tails.immerda.ch/whisperback/tree/whisperBack/encryption.py?h=feature/python3
   * http://www.physics.drexel.edu/~wking/code/python/send_pgp_mime
 """
 
   * https://git-tails.immerda.ch/whisperback/tree/whisperBack/encryption.py?h=feature/python3
   * http://www.physics.drexel.edu/~wking/code/python/send_pgp_mime
 """
 
-import sys
-import gpgme
 import re
 import io
 import os
 import re
 import io
 import os
+import sys
+import enum
+import gpgme
+import smtplib
 import importlib
 
 import email.parser
 import email.message
 import email.encoders
 
 import importlib
 
 import email.parser
 import email.message
 import email.encoders
 
+from email.mime.text            import MIMEText
 from email.mime.multipart       import MIMEMultipart
 from email.mime.application     import MIMEApplication
 from email.mime.nonmultipart    import MIMENonMultipart
 
 import edward_config
 
 from email.mime.multipart       import MIMEMultipart
 from email.mime.application     import MIMEApplication
 from email.mime.nonmultipart    import MIMENonMultipart
 
 import edward_config
 
-langs = ["de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
+langs = ["de", "el", "en", "es", "fr", "it", "ja", "pt-br", "ro", "ru", "tr", "zh"]
 
 """This list contains the abbreviated names of reply languages available to
 edward."""
 
 
 """This list contains the abbreviated names of reply languages available to
 edward."""
 
+class TxtType (enum.Enum):
+    text        = 0
+    message     = 1
+    pubkey      = 2
+    detachedsig = 3
+    signature   = 4
+
 
 
-match_types =  [('clearsign',
-                '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----'),
-                ('message',
+match_pairs =  [(TxtType.message,
                 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
                 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
-                ('pubkey',
+                (TxtType.pubkey,
                 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
                 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
-                ('detachedsig',
+                (TxtType.detachedsig,
                 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
 
 """This list of tuples matches query names with re.search() queries used
                 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
 
 """This list of tuples matches query names with re.search() queries used
@@ -78,16 +86,15 @@ class EddyMsg (object):
     'subparts' points to a list of mime sub-parts if it is a multi-part
     message. Otherwise it points to an empty list.
 
     'subparts' points to a list of mime sub-parts if it is a multi-part
     message. Otherwise it points to an empty list.
 
-    'payload_bytes' contains the raw mime-decoded bytes that haven't been
-    encoded into a character set.
+    'payload_bytes' is a binary representation of the mime part before header
+    removal and message decoding.
 
     'payload_pieces' is a list of objects containing strings that when strung
     together form the fully-decoded string representation of the mime part.
 
 
     'payload_pieces' is a list of objects containing strings that when strung
     together form the fully-decoded string representation of the mime part.
 
-    The 'charset' describes the character set of payload_bytes.
+    The 'filename', 'content_type', 'content_disposition' and
+    'description_list' come from the mime part parameters.
 
 
-    The 'filename', 'content_type' and 'description_list' come from the mime
-    part parameters.
     """
 
     multipart               = False
     """
 
     multipart               = False
@@ -96,9 +103,9 @@ class EddyMsg (object):
     payload_bytes           = None
     payload_pieces          = []
 
     payload_bytes           = None
     payload_pieces          = []
 
-    charset                 = None
     filename                = None
     content_type            = None
     filename                = None
     content_type            = None
+    content_disposition     = None
     description_list        = None
 
 
     description_list        = None
 
 
@@ -109,11 +116,11 @@ class PayloadPiece (object):
     Instances of this class are often strung together within one or more arrays
     pointed to by each instance of the EddyMsg class.
 
     Instances of this class are often strung together within one or more arrays
     pointed to by each instance of the EddyMsg class.
 
-    'piece_type' refers to a string whose value describes the content of
-    'string'.  Examples include "pubkey", for public keys, and "message", for
-    encrypted data (or armored signatures until they are known to be such.) The
-    names derive from the header and footer of each of these ascii-encoded gpg
-    blocks.
+    'piece_type' refers to an enum whose value describes the content of
+    'string'.  Examples include TxtType.pubkey, for public keys, and
+    TxtType.message, for encrypted data (or armored signatures until they are
+    known to be such.) Some of the names derive from the header and footer of
+    each of these ascii-encoded gpg blocks.
 
     'string' contains some string of text, such as non-GPG text, an encrypted
     block of text, a signature, or a public key.
 
     'string' contains some string of text, such as non-GPG text, an encrypted
     block of text, a signature, or a public key.
@@ -141,6 +148,12 @@ class GPGData (object):
 
     'sigs' is a list of fingerprints of keys used to sign the data in plainobj.
 
 
     'sigs' is a list of fingerprints of keys used to sign the data in plainobj.
 
+    'sigkey_missing' is set to True if edward doesn't have the key it needs to
+    verify the signature on a block of text.
+
+    'key_cannot_encrypt' is set to True if pubkeys or sigs' keys in the payload
+    piece are not capable of encryption, are revoked or expired, for instance.
+
     'keys' is a list of fingerprints of keys obtained in public key blocks.
     """
 
     'keys' is a list of fingerprints of keys obtained in public key blocks.
     """
 
@@ -148,6 +161,8 @@ class GPGData (object):
 
     plainobj                = None
     sigs                    = []
 
     plainobj                = None
     sigs                    = []
+    sigkey_missing          = False
+    key_cannot_encrypt      = False
     keys                    = []
 
 
     keys                    = []
 
 
@@ -170,42 +185,55 @@ class ReplyInfo (object):
     unencrypted data; alternatively it may be a public key attached to the
     message.
 
     unencrypted data; alternatively it may be a public key attached to the
     message.
 
+    'encrypt_to_key' the key object to use when encrypting edward's reply
+
     'msg_to_quote' refers to the part of a message which edward should quote in
     his reply. This should remain as None if there was no encrypted and singed
     part. This is to avoid making edward a service for decrypting other
     people's messages to edward.
 
     'msg_to_quote' refers to the part of a message which edward should quote in
     his reply. This should remain as None if there was no encrypted and singed
     part. This is to avoid making edward a service for decrypting other
     people's messages to edward.
 
-    'success_decrypt' is set to True if edward could decrypt part of the
+    'decrypt_success' is set to True if edward could decrypt part of the
     message.
 
     message.
 
-    'failed_decrypt' is set to True if edward failed to decrypt part of the
-    message.
+    'sig_success' is set to True if edward could to some extent verify the
+    signature of a signed part of the message to edward.
 
 
-    'publick_key_received' is set to True if edward successfully imported a
-    public key.
+    'key_can_encrypt' is set to True if a key which can be encrypted to has
+    been found.
 
 
-    'no_public_key' is set to True if edward doesn't have a key to encrypt to
-    when replying to the user.
+    'sig_failure' is set to True if edward could not verify a siganture.
 
 
-    'sig_success' is set to True if edward could to some extent verify the
-    signature of a signed part of the message to edward.
+    'pubkey_success' is set to True if edward successfully imported a public
+    key.
 
 
-    'sig_failure' is set to True if edward failed to some extent verify the
-    signature of a signed part of the message to edward.
+    'sigkey_missing' is set to True if edward doesn't have the public key
+    needed for signature verification.
+
+    'key_cannot_encrypt' is set to True if pubkeys or sig's keys in a payload
+    piece of the message are not capable of encryption.
+
+    'have_repy_key' is set to True if edward has a public key to encrypt its
+    reply to.
     """
 
     replies                 = None
 
     target_key              = None
     fallback_target_key     = None
     """
 
     replies                 = None
 
     target_key              = None
     fallback_target_key     = None
+    encrypt_to_key          = None
     msg_to_quote            = ""
 
     msg_to_quote            = ""
 
-    success_decrypt         = False
-    failed_decrypt          = False
-    public_key_received     = False
-    no_public_key           = False
+    decrypt_success         = False
     sig_success             = False
     sig_success             = False
+    pubkey_success          = False
+    key_can_encrypt         = False
+
+    decrypt_failure         = False
     sig_failure             = False
     sig_failure             = False
+    sigkey_missing          = False
+    key_cannot_encrypt      = False
+
+    have_reply_key          = False
 
 
 def main ():
 
 
 def main ():
@@ -237,29 +265,37 @@ def main ():
         implied by the To: address in the original email.
     """
 
         implied by the To: address in the original email.
     """
 
-    handle_args()
+    print_reply_only = handle_args()
+
+    email_bytes = sys.stdin.buffer.read()
+
+    test_auto_reply(email_bytes)
 
     gpgme_ctx = get_gpg_context(edward_config.gnupghome,
                               edward_config.sign_with_key)
 
 
     gpgme_ctx = get_gpg_context(edward_config.gnupghome,
                               edward_config.sign_with_key)
 
-    email_text = sys.stdin.read()
-    email_struct = parse_pgp_mime(email_text, gpgme_ctx)
+    # do this twice so sigs can be verified with newly imported keys
+    parse_pgp_mime(email_bytes, gpgme_ctx)
+    email_struct = parse_pgp_mime(email_bytes, gpgme_ctx)
 
 
-    email_to, email_from, email_subject = email_to_from_subject(email_text)
-    lang = import_lang(email_to)
+    email_to, email_reply_to, email_subject = email_to_reply_to_subject(email_bytes)
+    lang, reply_from = import_lang_pick_address(email_to, edward_config.hostname)
 
     replyinfo_obj = ReplyInfo()
     replyinfo_obj.replies = lang.replies
 
     prepare_for_reply(email_struct, replyinfo_obj)
 
     replyinfo_obj = ReplyInfo()
     replyinfo_obj.replies = lang.replies
 
     prepare_for_reply(email_struct, replyinfo_obj)
-    encrypt_to_key = get_key_from_fp(replyinfo_obj, gpgme_ctx)
+    get_key_from_fp(replyinfo_obj, gpgme_ctx)
     reply_plaintext = write_reply(replyinfo_obj)
 
     reply_plaintext = write_reply(replyinfo_obj)
 
-    reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \
-                                         email_subject, encrypt_to_key,
+    reply_mime = generate_encrypted_mime(reply_plaintext, email_reply_to, reply_from, \
+                                         email_subject, replyinfo_obj.encrypt_to_key,
                                          gpgme_ctx)
 
                                          gpgme_ctx)
 
-    print(reply_mime)
+    if print_reply_only == True:
+        print(reply_mime)
+    else:
+        send_reply(reply_mime, email_reply_to, reply_from)
 
 
 def get_gpg_context (gnupghome, sign_with_key_fp):
 
 
 def get_gpg_context (gnupghome, sign_with_key_fp):
@@ -287,7 +323,7 @@ def get_gpg_context (gnupghome, sign_with_key_fp):
 
     try:
         sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
 
     try:
         sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
-    except:
+    except gpgme.GpgmeError:
         error("unable to load signing key. is the gnupghome "
                 + "and signing key properly set in the edward_config.py?")
         exit(1)
         error("unable to load signing key. is the gnupghome "
                 + "and signing key properly set in the edward_config.py?")
         exit(1)
@@ -297,7 +333,7 @@ def get_gpg_context (gnupghome, sign_with_key_fp):
     return gpgme_ctx
 
 
     return gpgme_ctx
 
 
-def parse_pgp_mime (email_text, gpgme_ctx):
+def parse_pgp_mime (email_bytes, gpgme_ctx):
     """Parses the email for mime payloads and decrypts/verfies signatures.
 
     This function creates a representation of a mime or plaintext email with
     """Parses the email for mime payloads and decrypts/verfies signatures.
 
     This function creates a representation of a mime or plaintext email with
@@ -306,7 +342,7 @@ def parse_pgp_mime (email_text, gpgme_ctx):
     does some very basic signature verification on those parts.
 
     Args:
     does some very basic signature verification on those parts.
 
     Args:
-        email_text: an email message in string format
+        email_bytes: an email message in byte string format
         gpgme_ctx:  a gpgme context
 
     Returns:
         gpgme_ctx:  a gpgme context
 
     Returns:
@@ -317,7 +353,7 @@ def parse_pgp_mime (email_text, gpgme_ctx):
         imported payloads
     """
 
         imported payloads
     """
 
-    email_struct = email.parser.Parser().parsestr(email_text)
+    email_struct = email.parser.BytesParser().parsebytes(email_bytes)
 
     eddymsg_obj = parse_mime(email_struct)
     split_payloads(eddymsg_obj)
 
     eddymsg_obj = parse_mime(email_struct)
     split_payloads(eddymsg_obj)
@@ -333,14 +369,14 @@ def parse_mime(msg_struct):
     each sub-part is also a EddyMsg instance.
 
     Args:
     each sub-part is also a EddyMsg instance.
 
     Args:
-        msg_struct: an email parsed with email.parser.Parser(), which can be
+        msg_struct: an email parsed with email.parser.BytesParser(), which can be
             multi-part
 
     Returns:
         an instance of EddyMsg, potentially a recursive one.
     """
 
             multi-part
 
     Returns:
         an instance of EddyMsg, potentially a recursive one.
     """
 
-    eddymsg_obj = EddyMsg()
+    eddymsg_obj = get_subpart_data(msg_struct)
 
     if msg_struct.is_multipart() == True:
         payloads = msg_struct.get_payload()
 
     if msg_struct.is_multipart() == True:
         payloads = msg_struct.get_payload()
@@ -348,13 +384,10 @@ def parse_mime(msg_struct):
         eddymsg_obj.multipart = True
         eddymsg_obj.subparts = list(map(parse_mime, payloads))
 
         eddymsg_obj.multipart = True
         eddymsg_obj.subparts = list(map(parse_mime, payloads))
 
-    else:
-        eddymsg_obj = get_subpart_data(msg_struct)
-
     return eddymsg_obj
 
 
     return eddymsg_obj
 
 
-def scan_and_split (payload_piece, match_type, pattern):
+def scan_and_split (payload_piece, match_name, pattern):
     """This splits the payloads of an EddyMsg object into GPG and text parts.
 
     An EddyMsg object's payload_pieces starts off as a list containing a single
     """This splits the payloads of an EddyMsg object into GPG and text parts.
 
     An EddyMsg object's payload_pieces starts off as a list containing a single
@@ -364,7 +397,7 @@ def scan_and_split (payload_piece, match_type, pattern):
 
     Args:
         payload_piece: a single payload or a split part of a payload
 
     Args:
         payload_piece: a single payload or a split part of a payload
-        match_type: the type of data to try to spit out from the payload piece
+        match_name: the type of data to try to spit out from the payload piece
         pattern: the search pattern to be used for finding that type of data
 
     Returns:
         pattern: the search pattern to be used for finding that type of data
 
     Returns:
@@ -374,12 +407,11 @@ def scan_and_split (payload_piece, match_type, pattern):
     """
 
     # don't try to re-split pieces containing gpg data
     """
 
     # don't try to re-split pieces containing gpg data
-    if payload_piece.piece_type != "text":
+    if payload_piece.piece_type != TxtType.text:
         return [payload_piece]
 
     flags = re.DOTALL | re.MULTILINE
         return [payload_piece]
 
     flags = re.DOTALL | re.MULTILINE
-    matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
-                        ")(?P<rest>.*)", payload_piece.string, flags=flags)
+    matches = re.search(pattern, payload_piece.string, flags=flags)
 
     if matches == None:
         pieces = [payload_piece]
 
     if matches == None:
         pieces = [payload_piece]
@@ -387,54 +419,61 @@ def scan_and_split (payload_piece, match_type, pattern):
     else:
 
         beginning               = PayloadPiece()
     else:
 
         beginning               = PayloadPiece()
-        beginning.string        = matches.group('beginning')
+        beginning.string        = payload_piece.string[:matches.start()]
         beginning.piece_type    = payload_piece.piece_type
 
         match                   = PayloadPiece()
         beginning.piece_type    = payload_piece.piece_type
 
         match                   = PayloadPiece()
-        match.string            = matches.group('match')
-        match.piece_type        = match_type
+        match.string            = payload_piece.string[matches.start():matches.end()]
+        match.piece_type        = match_name
 
         rest                    = PayloadPiece()
 
         rest                    = PayloadPiece()
-        rest.string             = matches.group('rest')
+        rest.string             = payload_piece.string[matches.end():]
         rest.piece_type         = payload_piece.piece_type
 
         rest.piece_type         = payload_piece.piece_type
 
-        more_pieces = scan_and_split(rest, match_type, pattern)
+        more_pieces = scan_and_split(rest, match_name, pattern)
         pieces = [beginning, match ] + more_pieces
 
     return pieces
 
 
 def get_subpart_data (part):
         pieces = [beginning, match ] + more_pieces
 
     return pieces
 
 
 def get_subpart_data (part):
-    """This function grabs information from a single part mime object.
+    """This function grabs information from a mime part.
 
 
-    It copies needed data from a single part email.parser.Parser() object over
-    to an EddyMsg object.
+    It copies needed data from an email.parser.BytesParser() object over to an
+    EddyMsg object.
 
     Args:
 
     Args:
-        part: a non-multi-part mime.parser.Parser() object
+        part: an email.parser.BytesParser() object
 
     Returns:
 
     Returns:
-        a single-part EddyMsg() object
+        an EddyMsg() object
     """
 
     obj = EddyMsg()
 
     """
 
     obj = EddyMsg()
 
-    obj.charset             = part.get_content_charset()
-    obj.payload_bytes       = part.get_payload(decode=True)
+    mime_decoded_bytes      = part.get_payload(decode=True)
+    charset                 = part.get_content_charset()
+
+    # your guess is as good as a-myy-ee-ine...
+    if charset == None:
+        charset = 'utf-8'
+
+    payload_string          = part.as_string()
+    if payload_string != None:
+        # convert each isolated carriage return or line feed to carriage return + line feed
+        payload_string_crlf = re.sub(r'\n', '\r\n', re.sub(r'\r', '\n', re.sub(r'\r\n', '\n', payload_string)))
+        obj.payload_bytes   = payload_string_crlf.encode(charset)
 
     obj.filename            = part.get_filename()
     obj.content_type        = part.get_content_type()
 
     obj.filename            = part.get_filename()
     obj.content_type        = part.get_content_type()
+    obj.content_disposition = part['content-disposition']
     obj.description_list    = part['content-description']
 
     obj.description_list    = part['content-description']
 
-    # your guess is as good as a-myy-ee-ine...
-    if obj.charset == None:
-        obj.charset = 'utf-8'
-
-    if obj.payload_bytes != None:
+    if mime_decoded_bytes != None:
         try:
             payload = PayloadPiece()
         try:
             payload = PayloadPiece()
-            payload.string = obj.payload_bytes.decode(obj.charset)
-            payload.piece_type = 'text'
+            payload.string = mime_decoded_bytes.decode(charset)
+            payload.piece_type = TxtType.text
 
             obj.payload_pieces = [payload]
         except UnicodeDecodeError:
 
             obj.payload_pieces = [payload]
         except UnicodeDecodeError:
@@ -491,11 +530,11 @@ def split_payloads (eddymsg_obj):
         The EddyMsg object's payloads are all split into GPG and non-GPG parts.
     """
 
         The EddyMsg object's payloads are all split into GPG and non-GPG parts.
     """
 
-    for match_type in match_types:
-        do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_type)
+    for match_pair in match_pairs:
+        do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_pair)
 
 
 
 
-def split_payload_pieces (eddymsg_obj, match_type):
+def split_payload_pieces (eddymsg_obj, match_pair):
     """A helper function for split_payloads(); works on PayloadPiece objects.
 
     This function splits up PayloadPiece objects into multipe PayloadPiece
     """A helper function for split_payloads(); works on PayloadPiece objects.
 
     This function splits up PayloadPiece objects into multipe PayloadPiece
@@ -504,7 +543,7 @@ def split_payload_pieces (eddymsg_obj, match_type):
 
     Args:
         eddymsg_obj: a single-part EddyMsg object.
 
     Args:
         eddymsg_obj: a single-part EddyMsg object.
-        match_type: a tuple from the match_types list, which specifies a match
+        match_pair: a tuple from the match_pairs list, which specifies a match
             name and a match pattern.
 
     Returns:
             name and a match pattern.
 
     Returns:
@@ -516,10 +555,10 @@ def split_payload_pieces (eddymsg_obj, match_type):
 
     Post:
         The EddyMsg object's payload piece(s) are split into a list of pieces
 
     Post:
         The EddyMsg object's payload piece(s) are split into a list of pieces
-        if matches of the match_type are found.
+        if matches of the match_pair are found.
     """
 
     """
 
-    (match_name, pattern) = match_type
+    (match_name, pattern) = match_pair
 
     new_pieces_list = []
     for piece in eddymsg_obj.payload_pieces:
 
     new_pieces_list = []
     for piece in eddymsg_obj.payload_pieces:
@@ -552,8 +591,9 @@ def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
         eddymsg_obj should have its payloads split into gpg and non-gpg pieces.
 
     Post:
         eddymsg_obj should have its payloads split into gpg and non-gpg pieces.
 
     Post:
-        Decryption, verification and key imports occur. the gpg_data member of
-        PayloadPiece objects get filled in with GPGData objects.
+        Decryption, verification and key imports occur. the gpg_data members of
+        PayloadPiece objects get filled in with GPGData objects with some of
+        their attributes set.
     """
 
     if eddymsg_obj.multipart == True:
     """
 
     if eddymsg_obj.multipart == True:
@@ -566,55 +606,57 @@ def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
 
     for piece in eddymsg_obj.payload_pieces:
 
 
     for piece in eddymsg_obj.payload_pieces:
 
-        if piece.piece_type == "text":
+        if piece.piece_type == TxtType.text:
             # don't transform the plaintext.
             pass
 
             # don't transform the plaintext.
             pass
 
-        elif piece.piece_type == "message":
-            (plaintext, sigs) = decrypt_block(piece.string, gpgme_ctx)
+        elif piece.piece_type == TxtType.message:
+            piece.gpg_data = GPGData()
+
+            (plaintext_b, sigs, sigkey_missing, key_cannot_encrypt) = decrypt_block(piece.string, gpgme_ctx)
 
 
-            if plaintext:
-                piece.gpg_data = GPGData()
+            piece.gpg_data.sigkey_missing = sigkey_missing
+            piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
+
+            if plaintext_b:
                 piece.gpg_data.decrypted = True
                 piece.gpg_data.sigs = sigs
                 # recurse!
                 piece.gpg_data.decrypted = True
                 piece.gpg_data.sigs = sigs
                 # recurse!
-                piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
+                piece.gpg_data.plainobj = parse_pgp_mime(plaintext_b, gpgme_ctx)
                 continue
 
             # if not encrypted, check to see if this is an armored signature.
                 continue
 
             # if not encrypted, check to see if this is an armored signature.
-            (plaintext, sigs) = verify_sig_message(piece.string, gpgme_ctx)
+            (plaintext_b, sigs,  sigkey_missing, key_cannot_encrypt) = verify_sig_message(piece.string, gpgme_ctx)
+
+            piece.gpg_data.sigkey_missing = sigkey_missing
+            piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
 
 
-            if plaintext:
-                piece.piece_type = "signature"
-                piece.gpg_data = GPGData()
+            if plaintext_b:
+                piece.piece_type = TxtType.signature
                 piece.gpg_data.sigs = sigs
                 # recurse!
                 piece.gpg_data.sigs = sigs
                 # recurse!
-                piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
+                piece.gpg_data.plainobj = parse_pgp_mime(plaintext_b, gpgme_ctx)
+
+        elif piece.piece_type == TxtType.pubkey:
+            piece.gpg_data = GPGData()
 
 
-        # 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)
+            (key_fps, key_cannot_encrypt) = add_gpg_key(piece.string, gpgme_ctx)
+
+            piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
 
             if key_fps != []:
 
             if key_fps != []:
-                piece.gpg_data = GPGData()
                 piece.gpg_data.keys = key_fps
 
                 piece.gpg_data.keys = key_fps
 
-        elif piece.piece_type == "clearsign":
-            (plaintext, sig_fps) = verify_clear_signature(piece.string, gpgme_ctx)
-
-            if sig_fps != []:
-                piece.gpg_data = GPGData()
-                piece.gpg_data.sigs = sig_fps
-                piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
+        elif piece.piece_type == TxtType.detachedsig:
+            piece.gpg_data = GPGData()
 
 
-        elif piece.piece_type == "detachedsig":
             for prev in prev_parts:
             for prev in prev_parts:
-                payload_bytes = prev.payload_bytes
-                sig_fps = verify_detached_signature(piece.string, payload_bytes, gpgme_ctx)
+                (sig_fps, sigkey_missing, key_cannot_encrypt) = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx)
+
+                piece.gpg_data.sigkey_missing = sigkey_missing
+                piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
 
                 if sig_fps != []:
 
                 if sig_fps != []:
-                    piece.gpg_data = GPGData()
                     piece.gpg_data.sigs = sig_fps
                     piece.gpg_data.plainobj = prev
                     break
                     piece.gpg_data.sigs = sig_fps
                     piece.gpg_data.plainobj = prev
                     break
@@ -672,19 +714,18 @@ def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
     """
 
     for piece in eddymsg_obj.payload_pieces:
     """
 
     for piece in eddymsg_obj.payload_pieces:
-        if piece.piece_type == "text":
+        if piece.piece_type == TxtType.text:
             # don't quote the plaintext part.
             pass
 
             # don't quote the plaintext part.
             pass
 
-        elif piece.piece_type == "message":
+        elif piece.piece_type == TxtType.message:
             prepare_for_reply_message(piece, replyinfo_obj)
 
             prepare_for_reply_message(piece, replyinfo_obj)
 
-        elif piece.piece_type == "pubkey":
+        elif piece.piece_type == TxtType.pubkey:
             prepare_for_reply_pubkey(piece, replyinfo_obj)
 
             prepare_for_reply_pubkey(piece, replyinfo_obj)
 
-        elif (piece.piece_type == "clearsign") \
-            or (piece.piece_type == "detachedsig") \
-            or (piece.piece_type == "signature"):
+        elif (piece.piece_type == TxtType.detachedsig) \
+            or (piece.piece_type == TxtType.signature):
                     prepare_for_reply_sig(piece, replyinfo_obj)
 
 
                     prepare_for_reply_sig(piece, replyinfo_obj)
 
 
@@ -692,43 +733,51 @@ def prepare_for_reply_message (piece, replyinfo_obj):
     """Helper function for prepare_for_reply()
 
     This function is called when the piece_type of a payload piece is
     """Helper function for prepare_for_reply()
 
     This function is called when the piece_type of a payload piece is
-    "message", or GPG Message block. This should be encrypted text. If the
-    encryted block is signed, a sig will be attached to .target_key unless
-    there is already one there.
+    TxtType.message, or GPG Message block. This should be encrypted text. If
+    the encryted block is correclty signed, a sig will be attached to
+    .target_key unless there is already one there.
 
     Args:
         piece: a PayloadPiece object.
         replyinfo_obj: object which gets updated with decryption status, etc.
 
 
     Args:
         piece: a PayloadPiece object.
         replyinfo_obj: object which gets updated with decryption status, etc.
 
-
     Returns:
         Nothing
 
     Pre:
     Returns:
         Nothing
 
     Pre:
-        the piece.payload_piece value should be "message".
+        the piece.payload_piece value should be TxtType.message.
 
     Post:
 
     Post:
-        replyinfo_obj gets updated with decryption status, signing status and a
-        potential signing key.
+        replyinfo_obj gets updated with decryption status, signing status, a
+        potential signing key, posession status of the public key for the
+        signature and encryption capability status if that key is missing.
     """
 
     """
 
-    if piece.gpg_data == None:
-        replyinfo_obj.failed_decrypt = True
+    if piece.gpg_data.plainobj == None:
+        replyinfo_obj.decrypt_failure = True
         return
 
         return
 
-    replyinfo_obj.success_decrypt = True
+    replyinfo_obj.decrypt_success = True
 
     # we already have a key (and a message)
     if replyinfo_obj.target_key != None:
         return
 
 
     # we already have a key (and a message)
     if replyinfo_obj.target_key != None:
         return
 
-    if piece.gpg_data.sigs != []:
-        replyinfo_obj.target_key = piece.gpg_data.sigs[0]
-        get_signed_part = False
-    else:
+    if piece.gpg_data.sigs == []:
+        if piece.gpg_data.sigkey_missing == True:
+            replyinfo_obj.sigkey_missing = True
+
+        if piece.gpg_data.key_cannot_encrypt == True:
+            replyinfo_obj.key_cannot_encrypt = True
+
         # only include a signed message in the reply.
         get_signed_part = True
 
         # only include a signed message in the reply.
         get_signed_part = True
 
+    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
     flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, get_signed_part)
 
     # to catch public keys in encrypted blocks
@@ -745,19 +794,20 @@ def prepare_for_reply_pubkey (piece, replyinfo_obj):
         replyinfo_obj: a ReplyInfo object
 
     Pre:
         replyinfo_obj: a ReplyInfo object
 
     Pre:
-        piece.piece_type should be set to "pubkey".
+        piece.piece_type should be set to TxtType.pubkey .
 
     Post:
         replyinfo_obj has its fields updated.
     """
 
 
     Post:
         replyinfo_obj has its fields updated.
     """
 
-    if piece.gpg_data == None or piece.gpg_data.keys == []:
-        replyinfo_obj.no_public_key = True
+    if piece.gpg_data.keys == []:
+        if piece.gpg_data.key_cannot_encrypt == True:
+            replyinfo_obj.key_cannot_encrypt = True
     else:
     else:
-        replyinfo_obj.public_key_received = True
+        replyinfo_obj.pubkey_success = True
 
 
-        if replyinfo_obj.fallback_target_key == None:
-            replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
+        # prefer public key as a fallback for the encrypted reply
+        replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
 
 
 def prepare_for_reply_sig (piece, replyinfo_obj):
 
 
 def prepare_for_reply_sig (piece, replyinfo_obj):
@@ -770,21 +820,32 @@ def prepare_for_reply_sig (piece, replyinfo_obj):
         replyinfo_obj: a ReplyInfo object
 
     Pre:
         replyinfo_obj: a ReplyInfo object
 
     Pre:
-        piece.piece_type should be set to "clearsign", "signature", or
-        "detachedsig".
+        piece.piece_type should be set to TxtType.signature, or
+        TxtType.detachedsig .
 
     Post:
         replyinfo_obj has its fields updated.
     """
 
 
     Post:
         replyinfo_obj has its fields updated.
     """
 
-    if piece.gpg_data == None or piece.gpg_data.sigs == []:
+    if piece.gpg_data.sigs == []:
         replyinfo_obj.sig_failure = True
         replyinfo_obj.sig_failure = True
+
+        if piece.gpg_data.sigkey_missing == True:
+            replyinfo_obj.sigkey_missing = True
+
+        if piece.gpg_data.key_cannot_encrypt == True:
+            replyinfo_obj.key_cannot_encrypt = True
+
     else:
         replyinfo_obj.sig_success = True
 
         if replyinfo_obj.fallback_target_key == None:
             replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
 
     else:
         replyinfo_obj.sig_success = True
 
         if replyinfo_obj.fallback_target_key == None:
             replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
 
+    if (piece.piece_type == TxtType.signature):
+        # to catch public keys in signature blocks
+        prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
+
 
 def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part):
     """For creating a string representation of a signed, encrypted part.
 
 def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part):
     """For creating a string representation of a signed, encrypted part.
@@ -827,31 +888,32 @@ def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part):
 
     for piece in eddymsg_obj.payload_pieces:
         if (get_signed_part):
 
     for piece in eddymsg_obj.payload_pieces:
         if (get_signed_part):
-            if ((piece.piece_type == "clearsign") \
-                    or (piece.piece_type == "detachedsig") \
-                    or (piece.piece_type == "signature")) \
-                    and (piece.gpg_data != None):
+            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:
                         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":
+            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
 
                 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.
+    If the .target_key is not set, then we use .fallback_target_key, if
+    available.
 
     Args:
         replyinfo_obj: ReplyInfo instance
         gpgme_ctx: the gpgme context
 
     Return:
 
     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.
+        Nothing
 
     Pre:
         Loading a key requires that we have the public key imported. This
 
     Pre:
         Loading a key requires that we have the public key imported. This
@@ -859,29 +921,30 @@ def get_key_from_fp (replyinfo_obj, gpgme_ctx):
         previously sent to edward.
 
     Post:
         previously sent to edward.
 
     Post:
-        If the key cannot be loaded, then the replyinfo_obj is marked for
-        having no public key available.
+        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.
     """
 
     """
 
-    if replyinfo_obj.target_key == None:
-        replyinfo_obj.target_key = replyinfo_obj.fallback_target_key
+    for key in (replyinfo_obj.target_key, replyinfo_obj.fallback_target_key):
+        if key != None:
+            try:
+                encrypt_to_key = gpgme_ctx.get_key(key)
 
 
-    if replyinfo_obj.target_key != None:
-        try:
-            encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key)
-            return encrypt_to_key
-
-        except:
-            pass
+            except gpgme.GpgmeError:
+                continue
 
 
-    # no available key to use
-    replyinfo_obj.target_key = None
-    replyinfo_obj.fallback_target_key = None
+            if is_key_usable(encrypt_to_key):
+                replyinfo_obj.encrypt_to_key = encrypt_to_key
+                replyinfo_obj.have_reply_key = True
+                replyinfo_obj.key_can_encrypt = True
+                return
 
 
-    replyinfo_obj.no_public_key = True
-    replyinfo_obj.public_key_received = False
+            else:
+                replyinfo_obj.key_cannot_encrypt = True
 
 
-    return None
 
 
 def write_reply (replyinfo_obj):
 
 
 def write_reply (replyinfo_obj):
@@ -904,37 +967,69 @@ def write_reply (replyinfo_obj):
 
     reply_plain = ""
 
 
     reply_plain = ""
 
-    if replyinfo_obj.success_decrypt == True:
-        reply_plain += replyinfo_obj.replies['success_decrypt']
+    if (replyinfo_obj.pubkey_success == True):
+        reply_plain += replyinfo_obj.replies['greeting']
+        reply_plain += "\n\n"
+
 
 
-        if replyinfo_obj.no_public_key == False:
-            quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
-            reply_plain += quoted_text
+    if replyinfo_obj.decrypt_success == True:
+        debug('decrypt success')
+        reply_plain += replyinfo_obj.replies['success_decrypt']
+        reply_plain += "\n\n"
 
 
-    elif replyinfo_obj.failed_decrypt == True:
+    elif replyinfo_obj.decrypt_failure == True:
+        debug('decrypt failure')
         reply_plain += replyinfo_obj.replies['failed_decrypt']
         reply_plain += replyinfo_obj.replies['failed_decrypt']
+        reply_plain += "\n\n"
 
 
     if replyinfo_obj.sig_success == True:
 
 
     if replyinfo_obj.sig_success == True:
-        reply_plain += "\n\n"
+        debug('signature success')
         reply_plain += replyinfo_obj.replies['sig_success']
         reply_plain += replyinfo_obj.replies['sig_success']
+        reply_plain += "\n\n"
 
     elif replyinfo_obj.sig_failure == True:
 
     elif replyinfo_obj.sig_failure == True:
-        reply_plain += "\n\n"
+        debug('signature failure')
         reply_plain += replyinfo_obj.replies['sig_failure']
         reply_plain += replyinfo_obj.replies['sig_failure']
+        reply_plain += "\n\n"
 
 
 
 
-    if replyinfo_obj.public_key_received == True:
-        reply_plain += "\n\n"
+    if (replyinfo_obj.pubkey_success == True):
+        debug('public key received')
         reply_plain += replyinfo_obj.replies['public_key_received']
         reply_plain += replyinfo_obj.replies['public_key_received']
+        reply_plain += "\n\n"
 
 
-    elif replyinfo_obj.no_public_key == True:
+    elif (replyinfo_obj.sigkey_missing == True):
+        debug('no public key')
+        reply_plain += replyinfo_obj.replies['no_public_key']
         reply_plain += "\n\n"
         reply_plain += "\n\n"
+
+    elif (replyinfo_obj.key_can_encrypt == False) \
+            and (replyinfo_obj.key_cannot_encrypt == True):
+        debug('bad public key')
         reply_plain += replyinfo_obj.replies['no_public_key']
         reply_plain += replyinfo_obj.replies['no_public_key']
+        reply_plain += "\n\n"
+
+
+    if (replyinfo_obj.decrypt_success == True) \
+            and (replyinfo_obj.sig_success == True) \
+            and (replyinfo_obj.have_reply_key == True):
+        debug('message quoted')
+        reply_plain += replyinfo_obj.replies['quote_follows']
+        reply_plain += "\n\n"
+        quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
+        reply_plain += quoted_text
+        reply_plain += "\n\n"
+
+
+    if (reply_plain == ""):
+        debug('plaintext message')
+        reply_plain += replyinfo_obj.replies['failed_decrypt']
+        reply_plain += "\n\n"
 
 
 
 
-    reply_plain += "\n\n"
     reply_plain += replyinfo_obj.replies['signature']
     reply_plain += replyinfo_obj.replies['signature']
+    reply_plain += "\n\n"
 
     return reply_plain
 
 
     return reply_plain
 
@@ -950,24 +1045,40 @@ def add_gpg_key (key_block, gpgme_ctx):
         gpgme_ctx: the gpgme context
 
     Returns:
         gpgme_ctx: the gpgme context
 
     Returns:
-        the fingerprint(s) of the imported key(s)
+        the fingerprint(s) of the imported key(s) which can be used for
+        encryption, and a boolean marking whether none of the keys are capable
+        of encryption.
     """
 
     fp = io.BytesIO(key_block.encode('ascii'))
 
     """
 
     fp = io.BytesIO(key_block.encode('ascii'))
 
-    result = gpgme_ctx.import_(fp)
-    imports = result.imports
+    try:
+        result = gpgme_ctx.import_(fp)
+        imports = result.imports
+    except gpgme.GpgmeError:
+        imports = []
 
     key_fingerprints = []
 
     key_fingerprints = []
+    key_cannot_encrypt = False
+
+    for import_res in imports:
+        fingerprint = import_res[0]
 
 
-    if imports != []:
-        for import_ in imports:
-            fingerprint = import_[0]
+        try:
+            key_obj = gpgme_ctx.get_key(fingerprint)
+        except:
+            key_obj = None
+
+        if key_obj != None and is_key_usable(key_obj):
             key_fingerprints += [fingerprint]
             key_fingerprints += [fingerprint]
+            key_cannot_encrypt = False
 
             debug("added gpg key: " + fingerprint)
 
 
             debug("added gpg key: " + fingerprint)
 
-    return key_fingerprints
+        elif key_fingerprints == []:
+            key_cannot_encrypt = True
+
+    return (key_fingerprints, key_cannot_encrypt)
 
 
 def verify_sig_message (msg_block, gpgme_ctx):
 
 
 def verify_sig_message (msg_block, gpgme_ctx):
@@ -983,10 +1094,12 @@ def verify_sig_message (msg_block, gpgme_ctx):
         gpgme_ctx: the gpgme context
 
     Returns:
         gpgme_ctx: the gpgme context
 
     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.
+        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'))
     """
 
     block_b = io.BytesIO(msg_block.encode('ascii'))
@@ -994,48 +1107,14 @@ def verify_sig_message (msg_block, gpgme_ctx):
 
     try:
         sigs = gpgme_ctx.verify(block_b, None, plain_b)
 
     try:
         sigs = gpgme_ctx.verify(block_b, None, plain_b)
-    except:
-        return ("",[])
-
-    plaintext = plain_b.getvalue().decode('utf-8')
+    except gpgme.GpgmeError:
+        return ("",[],False,False)
 
 
-    fingerprints = []
-    for sig in sigs:
-        fingerprints += [sig.fpr]
-    return (plaintext, fingerprints)
-
-
-def verify_clear_signature (sig_block, gpgme_ctx):
-    """Verifies the signature of a clear signature.
-
-    It first encodes the string into utf-8, but this will need to be fixed in
-    order to support other character encodings.
-
-    Args:
-        sig_block: a string of clear-signed text.
-        gpgme_ctx: the gpgme context
-
-    Returns:
-        A tuple of the plaintext of the signed part and the list of
-        fingerprints of keys signing the data. If verification failed, then
-        empty results are returned.
-    """
-
-    # 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()
+    plaintext_b = plain_b.getvalue()
 
 
-    result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
+    (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx)
 
 
-    # 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]
-
-    return plaintext, sig_fingerprints
+    return (plaintext_b, fingerprints, sigkey_missing, key_cannot_encrypt)
 
 
 def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
 
 
 def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
@@ -1049,25 +1128,29 @@ def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
         gpgme_ctx: the gpgme context
 
     Returns:
         gpgme_ctx: the gpgme context
 
     Returns:
-        A list of signing fingerprints if the signature verification was
-        sucessful. Otherwise, an empty list is returned.
+        A tuple containging a list of encryption capable signing fingerprints
+        if the signature verification was sucessful, a boolean marking whether
+        edward is missing all public keys for validating any of the signatures,
+        and a boolean marking whether all signing keys are incapable of
+        encryption.  Otherwise, a tuple containing an empty list and True are
+        returned.
     """
 
     detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
     plaintext_fp = io.BytesIO(plaintext_bytes)
     """
 
     detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
     plaintext_fp = io.BytesIO(plaintext_bytes)
-    ptxt_fp = io.BytesIO()
 
 
-    result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
+    try:
+        sigs = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
+    except gpgme.GpgmeError:
+        return ([],False,False)
 
 
-    sig_fingerprints = []
-    for res_ in result:
-        sig_fingerprints += [res_.fpr]
+    (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx)
 
 
-    return sig_fingerprints
+    return (fingerprints, sigkey_missing, key_cannot_encrypt)
 
 
 def decrypt_block (msg_block, gpgme_ctx):
 
 
 def decrypt_block (msg_block, gpgme_ctx):
-    """Decrypts a block of GPG text, and verifies any included sigatures.
+    """Decrypts a block of GPG text and verifies any included sigatures.
 
     Some encypted messages have embeded signatures, so those are verified too.
 
 
     Some encypted messages have embeded signatures, so those are verified too.
 
@@ -1076,8 +1159,11 @@ def decrypt_block (msg_block, gpgme_ctx):
         gpgme_ctx: the gpgme context
 
     Returns:
         gpgme_ctx: the gpgme context
 
     Returns:
-        A tuple of plaintext and signatures, if the decryption and signature
-        verification were successful, respectively.
+        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'))
     """
 
     block_b = io.BytesIO(msg_block.encode('ascii'))
@@ -1085,68 +1171,171 @@ def decrypt_block (msg_block, gpgme_ctx):
 
     try:
         sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
 
     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.
+
+    Args:
+        sigs: a signature verification result object list
+        gpgme_ctx: a gpgme context
 
 
-    plaintext = plain_b.getvalue().decode('utf-8')
+    Returns:
+        fingerprints: a list of fingerprints
+        sigkey_missing: a boolean marking whether public keys are missing for
+            all available signatures.
+        key_cannot_encrypt: a boolearn marking whether available public keys are
+            incapable of encryption.
+    """
 
 
+    sigkey_missing = False
+    key_cannot_encrypt = False
     fingerprints = []
     fingerprints = []
+
     for sig in sigs:
     for sig in sigs:
-        fingerprints += [sig.fpr]
-    return (plaintext, fingerprints)
+        if (sig.summary == 0) or (sig.summary & gpgme.SIGSUM_VALID != 0) or (sig.summary & gpgme.SIGSUM_GREEN != 0):
+            try:
+                key_obj = gpgme_ctx.get_key(sig.fpr)
+            except:
+                if fingerprints == []:
+                    sigkey_missing = True
+                    continue
+
+            if is_key_usable(key_obj):
+                fingerprints += [sig.fpr]
+                key_cannot_encrypt = False
+                sigkey_missing = False
 
 
+            elif fingerprints == []:
+                key_cannot_encrypt = True
 
 
-def email_to_from_subject (email_text):
-    """Returns the values of the email's To:, From: and Subject: fields
+        elif fingerprints == []:
+            if (sig.summary & gpgme.SIGSUM_KEY_MISSING != 0):
+                sigkey_missing = True
+
+    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)
+
+
+def email_to_reply_to_subject (email_bytes):
+    """Returns the email's To:, Reply-To: (or From:), and Subject: fields
 
     Returns this information from an email.
 
     Args:
 
     Returns this information from an email.
 
     Args:
-        email_text: the string form of the email
+        email_bytes: the byte string form of the email
 
     Returns:
 
     Returns:
-        the email To:, From:, and Subject: fields as strings
+        the email To:, Reply-To: (or From:), and Subject: fields as strings
     """
 
     """
 
-    email_struct = email.parser.Parser().parsestr(email_text)
+    email_struct = email.parser.BytesHeaderParser().parsebytes(email_bytes)
 
     email_to        = email_struct['To']
     email_from      = email_struct['From']
 
     email_to        = email_struct['To']
     email_from      = email_struct['From']
+    email_reply_to  = email_struct['Reply-To']
+
     email_subject   = email_struct['Subject']
 
     email_subject   = email_struct['Subject']
 
-    return email_to, email_from, email_subject
+    if email_reply_to == None:
+        email_reply_to = email_from
+
+    return email_to, email_reply_to, email_subject
 
 
 
 
-def import_lang(email_to):
-    """Imports appropriate language file for basic i18n support
+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'
 
     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.
+    list at the beginning of the program. This function also chooses the
+    language-dependent address which can be used as the From address in the
+    reply email.
 
     Args:
         email_to: the string containing the email address that the mail was
 
     Args:
         email_to: the string containing the email address that the mail was
-        sent to.
+            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.
     """
 
 
     Returns:
         the reference to the imported language module. The only variable in
         this file is the 'replies' dictionary.
     """
 
+    # default
+    use_lang = "en"
+
     if email_to != None:
         for lang in langs:
             if "edward-" + lang in email_to:
     if email_to != None:
         for lang in langs:
             if "edward-" + lang in email_to:
-                lang = "lang." + re.sub('-', '_', lang)
-                language = importlib.import_module(lang)
+                use_lang = lang
+                break
 
 
-                return language
+    lang_mod_name = "lang." + re.sub('-', '_', use_lang)
+    lang_module = importlib.import_module(lang_mod_name)
 
 
-    return importlib.import_module("lang.en")
+    reply_from = "edward-" + use_lang + "@" + hostname
 
 
+    return lang_module, reply_from
 
 
-def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
-                    gpgme_ctx):
+
+def generate_encrypted_mime (plaintext, email_to, email_from, email_subject,
+                    encrypt_to_key, gpgme_ctx):
     """This function creates the mime email reply. It can encrypt the email.
 
     If the encrypt_key is included, then the email is encrypted and signed.
     """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.
@@ -1154,7 +1343,7 @@ def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_ke
 
     Args:
         plaintext: the plaintext body of the message to create.
 
     Args:
         plaintext: the plaintext body of the message to create.
-        email_from: the email address to reply to
+        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)
         email_subject: the subject to use in reply
         encrypt_to_key: the key object to use for encrypting the email. (or
             None)
@@ -1164,47 +1353,40 @@ def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_ke
         A string version of the mime message, possibly encrypted and signed.
     """
 
         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
-
-    # 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):
 
         encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
                                               encrypt_to_key,
                                               gpgme_ctx)
 
     if (encrypt_to_key != None):
 
         encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
                                               encrypt_to_key,
                                               gpgme_ctx)
-        gpg_payload = encrypted_text
 
 
-    else:
-        signed_text = sign_message(plaintext_mime.as_string(), gpgme_ctx)
-        gpg_payload = signed_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')
 
 
-    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')
+        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')
 
 
-    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')
+        message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
+        message_mime.attach(control_mime)
+        message_mime.attach(encoded_mime)
+        message_mime['Content-Disposition'] = 'inline'
 
 
-    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:
+        message_mime = plaintext_mime
 
 
+    message_mime['Auto-Submitted'] = 'auto-replied'
 
 
-    message_mime['To'] = email_from
+    message_mime['To'] = email_to
+    message_mime['From'] = email_from
     message_mime['Subject'] = email_subject
 
     reply = message_mime.as_string()
     message_mime['Subject'] = email_subject
 
     reply = message_mime.as_string()
@@ -1212,6 +1394,29 @@ def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_ke
     return reply
 
 
     return reply
 
 
+def send_reply(email_txt, reply_to, reply_from):
+    """Sends reply email
+
+    Sent to original sender
+
+    Args:
+        email_txt: message as a string
+        reply_to: recipient of reply
+        reply_from: edward's specific email address
+
+    Post:
+        Email is sent
+    """
+
+    if reply_to == None:
+        error("*** ERROR: No one to send email to.")
+        exit(1)
+
+    s = smtplib.SMTP('localhost')
+    s.sendmail(reply_from, reply_to, email_txt)
+    s.quit()
+
+
 def email_quote_text (text):
     """Quotes input text by inserting "> "s
 
 def email_quote_text (text):
     """Quotes input text by inserting "> "s
 
@@ -1255,29 +1460,6 @@ def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
     return encrypted_txt
 
 
     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
 
 def error (error_msg):
     """Write an error message to stdout
 
@@ -1317,30 +1499,40 @@ def debug (debug_msg):
 
 
 def handle_args ():
 
 
 def handle_args ():
-    """Sets the progname variable and complains about any arguments
+    """Sets the progname variable and processes optional argument
 
 
-    If there are any arguments, then edward complains and quits, because input
-    is read from stdin.
+    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:
 
     Args:
         None
 
     Returns:
-        None
+        True if edward should print arguments instead of mailing them,
+        otherwise it returns False.
 
     Post:
 
     Post:
-        Exits with error 1 if there are arguments, otherwise returns to the
-        calling function, such as main().
+        Exits with error 1 if there are more than two arguments, otherwise
+        returns the print_reply_only option.
     """
 
     global progname
     progname = sys.argv[0]
 
     """
 
     global progname
     progname = sys.argv[0]
 
-    if len(sys.argv) > 1:
-        print(progname + ": error, this program doesn't " \
-                "need any arguments.", file=sys.stderr)
+    print_reply_only = False
+
+    if len(sys.argv) > 2:
+        print(progname + " usage:  " + progname + " [-p]\n\n" \
+                + "        -p      print reply message to stdout, do not mail it\n", \
+                file=sys.stderr)
         exit(1)
 
         exit(1)
 
+    elif (len(sys.argv) == 2) and (sys.argv[1] == "-p"):
+        print_reply_only = True
+
+    return print_reply_only
+
 
 if __name__ == "__main__":
     """Executes main if this file is not loaded interactively"""
 
 if __name__ == "__main__":
     """Executes main if this file is not loaded interactively"""