* http://www.physics.drexel.edu/~wking/code/python/send_pgp_mime
"""
-import sys
-import gpgme
import re
import io
import os
+import sys
+import enum
+import gpgme
import importlib
+import subprocess
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
"""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 = [('message',
+match_pairs = [(TxtType.message,
'-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
- ('pubkey',
+ (TxtType.pubkey,
'-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
- ('detachedsig',
+ (TxtType.detachedsig,
'-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
"""This list of tuples matches query names with re.search() queries used
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.
implied by the To: address in the original email.
"""
- handle_args()
+ print_reply_only = handle_args()
gpgme_ctx = get_gpg_context(edward_config.gnupghome,
edward_config.sign_with_key)
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)
+ lang, reply_from = import_lang_pick_address(email_to, edward_config.hostname)
replyinfo_obj = ReplyInfo()
replyinfo_obj.replies = lang.replies
email_subject, encrypt_to_key,
gpgme_ctx)
- print(reply_mime)
+ if print_reply_only == True:
+ print(reply_mime)
+ else:
+ send_reply(reply_mime, email_subject, email_from, reply_from)
def get_gpg_context (gnupghome, sign_with_key_fp):
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
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:
"""
# 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
match = PayloadPiece()
match.string = matches.group('match')
- match.piece_type = match_type
+ match.piece_type = match_name
rest = PayloadPiece()
rest.string = matches.group('rest')
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
try:
payload = PayloadPiece()
payload.string = mime_decoded_bytes.decode(charset)
- payload.piece_type = 'text'
+ payload.piece_type = TxtType.text
obj.payload_pieces = [payload]
except UnicodeDecodeError:
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
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:
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:
for piece in eddymsg_obj.payload_pieces:
- if piece.piece_type == "text":
+ if piece.piece_type == TxtType.text:
# don't transform the plaintext.
pass
- elif piece.piece_type == "message":
+ elif piece.piece_type == TxtType.message:
(plaintext, sigs) = decrypt_block(piece.string, gpgme_ctx)
if plaintext:
(plaintext, sigs) = verify_sig_message(piece.string, gpgme_ctx)
if plaintext:
- piece.piece_type = "signature"
+ piece.piece_type = TxtType.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":
+ # FIXME: consider handling pubkeys first, so that signatures can be
+ # validated on freshly imported keys
+ elif piece.piece_type == TxtType.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":
+ elif piece.piece_type == TxtType.detachedsig:
for prev in prev_parts:
sig_fps = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx)
"""
for piece in eddymsg_obj.payload_pieces:
- if piece.piece_type == "text":
+ if piece.piece_type == TxtType.text:
# don't quote the plaintext part.
pass
- elif piece.piece_type == "message":
+ elif piece.piece_type == TxtType.message:
prepare_for_reply_message(piece, replyinfo_obj)
- elif piece.piece_type == "pubkey":
+ elif piece.piece_type == TxtType.pubkey:
prepare_for_reply_pubkey(piece, replyinfo_obj)
- elif (piece.piece_type == "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)
"""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
+ TxtType.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.
Nothing
Pre:
- the piece.payload_piece value should be "message".
+ the piece.payload_piece value should be TxtType.message.
Post:
replyinfo_obj gets updated with decryption status, signing status and a
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.
else:
replyinfo_obj.public_key_received = 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):
replyinfo_obj: a ReplyInfo object
Pre:
- piece.piece_type should be set to "signature", or "detachedsig".
+ piece.piece_type should be set to TxtType.signature, or
+ TxtType.detachedsig .
Post:
replyinfo_obj has its fields updated.
for piece in eddymsg_obj.payload_pieces:
if (get_signed_part):
- if ((piece.piece_type == "detachedsig") \
- or (piece.piece_type == "signature")) \
+ if ((piece.piece_type == TxtType.detachedsig) \
+ or (piece.piece_type == TxtType.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":
+ if piece.piece_type == TxtType.text:
replyinfo_obj.msg_to_quote += piece.string
reply_plain += "\n\n"
reply_plain += replyinfo_obj.replies['signature']
+ reply_plain += "\n"
return reply_plain
return email_to, email_from, 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'
- 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
- 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.
"""
+ # default
+ use_lang = "en"
+
if email_to != None:
for lang in langs:
if "edward-" + lang in email_to:
- lang = "lang." + re.sub('-', '_', lang)
- language = importlib.import_module(lang)
+ use_lang = lang
+ break
- 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_to, email_subject, encrypt_to_key,
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
+ if (encrypt_to_key != None):
- # 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)
+ # 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
- if (encrypt_to_key != None):
+ # 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)
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(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')
+ 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')
- 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 = MIMEText(plaintext)
message_mime['To'] = email_to
message_mime['Subject'] = email_subject
return reply
+def send_reply(email_txt, subject, reply_to, reply_from):
+
+ email_bytes = email_txt.encode('ascii')
+
+ p = subprocess.Popen(["/usr/sbin/sendmail", "-f", reply_from, "-F", "Edward, GPG Bot", "-i", reply_to], stdin=subprocess.PIPE)
+
+ (stdout, stderr) = p.communicate(email_bytes)
+
+ if stdout != None:
+ debug("sendmail stdout: " + str(stdout))
+ if stderr != None:
+ error("sendmail stderr: " + str(stderr))
+
+
def email_quote_text (text):
"""Quotes input text by inserting "> "s
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 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:
- None
+ True if edward should print arguments instead of mailing them,
+ otherwise it returns False.
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]
- if len(sys.argv) > 1:
- print(progname + ": error, this program doesn't " \
- "need any arguments.", file=sys.stderr)
+ print_reply_only = False
+
+ if len(sys.argv) > 2:
+ print(progname + " usage: " + progname + " [-p]\n\n" \
+ + " -p print reply message to stdout, do not mail it\n", \
+ file=sys.stderr)
exit(1)
+ elif (len(sys.argv) == 2) and (sys.argv[1] == "-p"):
+ print_reply_only = True
+
+ return print_reply_only
+
if __name__ == "__main__":
"""Executes main if this file is not loaded interactively"""