updated my personal email address
[edward.git] / edward
diff --git a/edward b/edward
index 2480fcd513a7a26b744a04df4d90a9d53e80a56f..d7443927cabc24fa373c66b8019015be5d981dd2 100755 (executable)
--- a/edward
+++ b/edward
@@ -26,7 +26,7 @@
 
 Code sourced from these projects:
 
-  * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/PREVIOUS-20150530/edward.tgz
+  * 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
 """
@@ -37,8 +37,8 @@ import os
 import sys
 import enum
 import gpgme
+import smtplib
 import importlib
-import subprocess
 
 import email.parser
 import email.message
@@ -51,7 +51,7 @@ from email.mime.nonmultipart    import MIMENonMultipart
 
 import edward_config
 
-langs = ["de", "el", "en", "es", "fr", "it", "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."""
@@ -92,8 +92,9 @@ class EddyMsg (object):
     '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.
+    The 'filename', 'content_type', 'content_disposition' and
+    'description_list' come from the mime part parameters.
+
     """
 
     multipart               = False
@@ -104,6 +105,7 @@ class EddyMsg (object):
 
     filename                = None
     content_type            = None
+    content_disposition     = None
     description_list        = None
 
 
@@ -265,13 +267,18 @@ def main ():
 
     print_reply_only = handle_args()
 
+    email_bytes = sys.stdin.buffer.read()
+
+    test_auto_reply(email_bytes)
+
     gpgme_ctx = get_gpg_context(edward_config.gnupghome,
                               edward_config.sign_with_key)
 
-    email_bytes = sys.stdin.buffer.read()
+    # 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_bytes)
+    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()
@@ -281,14 +288,14 @@ def main ():
     get_key_from_fp(replyinfo_obj, gpgme_ctx)
     reply_plaintext = write_reply(replyinfo_obj)
 
-    reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \
+    reply_mime = generate_encrypted_mime(reply_plaintext, email_reply_to, reply_from, \
                                          email_subject, replyinfo_obj.encrypt_to_key,
                                          gpgme_ctx)
 
     if print_reply_only == True:
         print(reply_mime)
     else:
-        send_reply(reply_mime, email_subject, email_from, reply_from)
+        send_reply(reply_mime, email_reply_to, reply_from)
 
 
 def get_gpg_context (gnupghome, sign_with_key_fp):
@@ -362,7 +369,7 @@ def parse_mime(msg_struct):
     each sub-part is also a EddyMsg instance.
 
     Args:
-        msg_struct: an email parsed with email.parser.Parser(), which can be
+        msg_struct: an email parsed with email.parser.BytesParser(), which can be
             multi-part
 
     Returns:
@@ -404,8 +411,7 @@ def scan_and_split (payload_piece, match_name, pattern):
         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]
@@ -413,15 +419,15 @@ def scan_and_split (payload_piece, match_name, pattern):
     else:
 
         beginning               = PayloadPiece()
-        beginning.string        = matches.group('beginning')
+        beginning.string        = payload_piece.string[:matches.start()]
         beginning.piece_type    = payload_piece.piece_type
 
         match                   = PayloadPiece()
-        match.string            = matches.group('match')
+        match.string            = payload_piece.string[matches.start():matches.end()]
         match.piece_type        = match_name
 
         rest                    = PayloadPiece()
-        rest.string             = matches.group('rest')
+        rest.string             = payload_piece.string[matches.end():]
         rest.piece_type         = payload_piece.piece_type
 
         more_pieces = scan_and_split(rest, match_name, pattern)
@@ -433,11 +439,11 @@ def scan_and_split (payload_piece, match_name, pattern):
 def get_subpart_data (part):
     """This function grabs information from a mime part.
 
-    It copies needed data from a email.parser.Parser() object over to an
+    It copies needed data from an email.parser.BytesParser() object over to an
     EddyMsg object.
 
     Args:
-        part: a mime.parser.Parser() object
+        part: an email.parser.BytesParser() object
 
     Returns:
         an EddyMsg() object
@@ -454,10 +460,13 @@ def get_subpart_data (part):
 
     payload_string          = part.as_string()
     if payload_string != None:
-        obj.payload_bytes   = payload_string.encode(charset)
+        # convert each isolated carriage return or line feed to carriage return + line feed
+        payload_string_crlf = re.sub(r'\n', '\r\n', re.sub(r'\r', '\n', re.sub(r'\r\n', '\n', payload_string)))
+        obj.payload_bytes   = payload_string_crlf.encode(charset)
 
     obj.filename            = part.get_filename()
     obj.content_type        = part.get_content_type()
+    obj.content_disposition = part['content-disposition']
     obj.description_list    = part['content-description']
 
     if mime_decoded_bytes != None:
@@ -887,7 +896,9 @@ def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part):
                         replyinfo_obj.target_key = piece.gpg_data.sigs[0]
                         break
         else:
-            if piece.piece_type == TxtType.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
 
 
@@ -964,15 +975,6 @@ def write_reply (replyinfo_obj):
     if replyinfo_obj.decrypt_success == True:
         debug('decrypt success')
         reply_plain += replyinfo_obj.replies['success_decrypt']
-
-        if (replyinfo_obj.sig_success == True) and (replyinfo_obj.have_reply_key == True):
-            debug('message quoted')
-            reply_plain += replyinfo_obj.replies['space']
-            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"
 
     elif replyinfo_obj.decrypt_failure == True:
@@ -1009,6 +1011,17 @@ def write_reply (replyinfo_obj):
         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']
@@ -1054,9 +1067,9 @@ def add_gpg_key (key_block, gpgme_ctx):
         try:
             key_obj = gpgme_ctx.get_key(fingerprint)
         except:
-            pass
+            key_obj = None
 
-        if is_key_usable(key_obj):
+        if key_obj != None and is_key_usable(key_obj):
             key_fingerprints += [fingerprint]
             key_cannot_encrypt = False
 
@@ -1232,8 +1245,34 @@ def is_key_usable (key_obj):
         return False
 
 
-def email_to_from_subject (email_bytes):
-    """Returns the values of the email's To:, From: and Subject: fields
+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.
 
@@ -1241,16 +1280,21 @@ def email_to_from_subject (email_bytes):
         email_bytes: the byte string form of the email
 
     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.BytesParser().parsebytes(email_bytes)
+    email_struct = email.parser.BytesHeaderParser().parsebytes(email_bytes)
 
     email_to        = email_struct['To']
     email_from      = email_struct['From']
+    email_reply_to  = email_struct['Reply-To']
+
     email_subject   = email_struct['Subject']
 
-    return email_to, email_from, email_subject
+    if email_reply_to == None:
+        email_reply_to = email_from
+
+    return email_to, email_reply_to, email_subject
 
 
 def import_lang_pick_address(email_to, hostname):
@@ -1290,8 +1334,8 @@ def import_lang_pick_address(email_to, hostname):
     return lang_module, reply_from
 
 
-def generate_encrypted_mime (plaintext, email_to, 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.
@@ -1339,7 +1383,10 @@ def generate_encrypted_mime (plaintext, email_to, email_subject, encrypt_to_key,
     else:
         message_mime = plaintext_mime
 
+    message_mime['Auto-Submitted'] = 'auto-replied'
+
     message_mime['To'] = email_to
+    message_mime['From'] = email_from
     message_mime['Subject'] = email_subject
 
     reply = message_mime.as_string()
@@ -1347,18 +1394,27 @@ def generate_encrypted_mime (plaintext, email_to, email_subject, encrypt_to_key,
     return reply
 
 
-def send_reply(email_txt, subject, reply_to, reply_from):
+def send_reply(email_txt, reply_to, reply_from):
+    """Sends reply email
+
+    Sent to original sender
 
-    email_bytes = email_txt.encode('ascii')
+    Args:
+        email_txt: message as a string
+        reply_to: recipient of reply
+        reply_from: edward's specific email address
 
-    p = subprocess.Popen(["/usr/sbin/sendmail", "-f", reply_from, "-F", "Edward, GPG Bot", "-i", reply_to], stdin=subprocess.PIPE)
+    Post:
+        Email is sent
+    """
 
-    (stdout, stderr) = p.communicate(email_bytes)
+    if reply_to == None:
+        error("*** ERROR: No one to send email to.")
+        exit(1)
 
-    if stdout != None:
-        debug("sendmail stdout: " + str(stdout))
-    if stderr != None:
-        error("sendmail stderr: " + str(stderr))
+    s = smtplib.SMTP('localhost')
+    s.sendmail(reply_from, reply_to, email_txt)
+    s.quit()
 
 
 def email_quote_text (text):