Code sourced from these projects:
- * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/CURRENT/edward.tar.gz
+ * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/PREVIOUS-20150530/edward.tar.gz
* https://git-tails.immerda.ch/whisperback/tree/whisperBack/encryption.py?h=feature/python3
* http://www.physics.drexel.edu/~wking/code/python/send_pgp_mime
"""
-import sys
-import gpgme
import re
import io
import os
+import sys
+import enum
+import gpgme
+import smtplib
import importlib
import email.parser
import email.message
import email.encoders
+from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.mime.nonmultipart import MIMENonMultipart
import edward_config
-langs = ["de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
+langs = ["de", "el", "en", "es", "fr", "it", "ja", "pt-br", "ro", "ru", "tr", "zh"]
"""This list contains the abbreviated names of reply languages available to
edward."""
+class TxtType (enum.Enum):
+ text = 0
+ message = 1
+ pubkey = 2
+ detachedsig = 3
+ signature = 4
-match_types = [('clearsign',
- '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----'),
- ('message',
+
+match_pairs = [(TxtType.message,
'-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
- ('pubkey',
+ (TxtType.pubkey,
'-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
- ('detachedsig',
+ (TxtType.detachedsig,
'-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
"""This list of tuples matches query names with re.search() queries used
'subparts' points to a list of mime sub-parts if it is a multi-part
message. Otherwise it points to an empty list.
- 'payload_bytes' contains the raw mime-decoded bytes that haven't been
- encoded into a character set.
+ 'payload_bytes' is a binary representation of the mime part before header
+ removal and message decoding.
'payload_pieces' is a list of objects containing strings that when strung
together form the fully-decoded string representation of the mime part.
- The 'charset' describes the character set of payload_bytes.
+ The 'filename', 'content_type', 'content_disposition' and
+ 'description_list' come from the mime part parameters.
- The 'filename', 'content_type' and 'description_list' come from the mime
- part parameters.
"""
multipart = False
payload_bytes = None
payload_pieces = []
- charset = None
filename = None
content_type = None
+ content_disposition = None
description_list = None
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.
'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.
"""
plainobj = None
sigs = []
+ sigkey_missing = False
+ key_cannot_encrypt = False
keys = []
unencrypted data; alternatively it may be a public key attached to the
message.
+ 'encrypt_to_key' the key object to use when encrypting edward's reply
+
'msg_to_quote' refers to the part of a message which edward should quote in
his reply. This should remain as None if there was no encrypted and singed
part. This is to avoid making edward a service for decrypting other
people's messages to edward.
- 'success_decrypt' is set to True if edward could decrypt part of the
+ 'decrypt_success' is set to True if edward could decrypt part of the
message.
- 'failed_decrypt' is set to True if edward failed to decrypt part of the
- message.
+ 'sig_success' is set to True if edward could to some extent verify the
+ signature of a signed part of the message to edward.
- 'publick_key_received' is set to True if edward successfully imported a
- public key.
+ 'key_can_encrypt' is set to True if a key which can be encrypted to has
+ been found.
- 'no_public_key' is set to True if edward doesn't have a key to encrypt to
- when replying to the user.
+ 'sig_failure' is set to True if edward could not verify a siganture.
- 'sig_success' is set to True if edward could to some extent verify the
- signature of a signed part of the message to edward.
+ 'pubkey_success' is set to True if edward successfully imported a public
+ key.
- 'sig_failure' is set to True if edward failed to some extent verify the
- signature of a signed part of the message to edward.
+ 'sigkey_missing' is set to True if edward doesn't have the public key
+ needed for signature verification.
+
+ 'key_cannot_encrypt' is set to True if pubkeys or sig's keys in a payload
+ piece of the message are not capable of encryption.
+
+ 'have_repy_key' is set to True if edward has a public key to encrypt its
+ reply to.
"""
replies = None
target_key = None
fallback_target_key = None
+ encrypt_to_key = None
msg_to_quote = ""
- success_decrypt = False
- failed_decrypt = False
- public_key_received = False
- no_public_key = False
+ decrypt_success = False
sig_success = False
+ pubkey_success = False
+ key_can_encrypt = False
+
+ decrypt_failure = False
sig_failure = False
+ sigkey_missing = False
+ key_cannot_encrypt = False
+
+ have_reply_key = False
def main ():
implied by the To: address in the original email.
"""
- handle_args()
+ print_reply_only = handle_args()
+
+ email_bytes = sys.stdin.buffer.read()
+
+ test_auto_reply(email_bytes)
gpgme_ctx = get_gpg_context(edward_config.gnupghome,
edward_config.sign_with_key)
- email_text = sys.stdin.read()
- email_struct = parse_pgp_mime(email_text, gpgme_ctx)
+ # do this twice so sigs can be verified with newly imported keys
+ parse_pgp_mime(email_bytes, gpgme_ctx)
+ email_struct = parse_pgp_mime(email_bytes, gpgme_ctx)
- email_to, email_from, email_subject = email_to_from_subject(email_text)
- lang = import_lang(email_to)
+ email_to, email_reply_to, email_subject = email_to_reply_to_subject(email_bytes)
+ lang, reply_from = import_lang_pick_address(email_to, edward_config.hostname)
replyinfo_obj = ReplyInfo()
replyinfo_obj.replies = lang.replies
prepare_for_reply(email_struct, replyinfo_obj)
- encrypt_to_key = get_key_from_fp(replyinfo_obj, gpgme_ctx)
+ get_key_from_fp(replyinfo_obj, gpgme_ctx)
reply_plaintext = write_reply(replyinfo_obj)
- reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \
- email_subject, encrypt_to_key,
+ reply_mime = generate_encrypted_mime(reply_plaintext, email_reply_to, reply_from, \
+ email_subject, replyinfo_obj.encrypt_to_key,
gpgme_ctx)
- print(reply_mime)
+ if print_reply_only == True:
+ print(reply_mime)
+ else:
+ send_reply(reply_mime, email_reply_to, reply_from)
def get_gpg_context (gnupghome, sign_with_key_fp):
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)
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
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:
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)
each sub-part is also a EddyMsg instance.
Args:
- msg_struct: an email parsed with email.parser.Parser(), which can be
+ msg_struct: an email parsed with email.parser.BytesParser(), which can be
multi-part
Returns:
an instance of EddyMsg, potentially a recursive one.
"""
- eddymsg_obj = EddyMsg()
+ eddymsg_obj = get_subpart_data(msg_struct)
if msg_struct.is_multipart() == True:
payloads = msg_struct.get_payload()
eddymsg_obj.multipart = True
eddymsg_obj.subparts = list(map(parse_mime, payloads))
- else:
- eddymsg_obj = get_subpart_data(msg_struct)
-
return eddymsg_obj
-def scan_and_split (payload_piece, match_type, pattern):
+def scan_and_split (payload_piece, match_name, pattern):
"""This splits the payloads of an EddyMsg object into GPG and text parts.
An EddyMsg object's payload_pieces starts off as a list containing a single
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
- 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]
else:
beginning = PayloadPiece()
- beginning.string = matches.group('beginning')
+ beginning.string = payload_piece.string[:matches.start()]
beginning.piece_type = payload_piece.piece_type
match = PayloadPiece()
- match.string = matches.group('match')
- match.piece_type = match_type
+ match.string = payload_piece.string[matches.start():matches.end()]
+ match.piece_type = match_name
rest = PayloadPiece()
- rest.string = matches.group('rest')
+ rest.string = payload_piece.string[matches.end():]
rest.piece_type = payload_piece.piece_type
- more_pieces = scan_and_split(rest, match_type, pattern)
+ more_pieces = scan_and_split(rest, match_name, pattern)
pieces = [beginning, match ] + more_pieces
return pieces
def get_subpart_data (part):
- """This function grabs information from a single part mime object.
+ """This function grabs information from a mime part.
- It copies needed data from a single part email.parser.Parser() object over
- to an EddyMsg object.
+ It copies needed data from an email.parser.BytesParser() object over to an
+ EddyMsg object.
Args:
- part: a non-multi-part mime.parser.Parser() object
+ part: an email.parser.BytesParser() object
Returns:
- a single-part EddyMsg() object
+ an EddyMsg() object
"""
obj = EddyMsg()
- obj.charset = part.get_content_charset()
- obj.payload_bytes = part.get_payload(decode=True)
+ mime_decoded_bytes = part.get_payload(decode=True)
+ charset = part.get_content_charset()
+
+ # your guess is as good as a-myy-ee-ine...
+ if charset == None:
+ charset = 'utf-8'
+
+ payload_string = part.as_string()
+ if payload_string != None:
+ # convert each isolated carriage return or line feed to carriage return + line feed
+ payload_string_crlf = re.sub(r'\n', '\r\n', re.sub(r'\r', '\n', re.sub(r'\r\n', '\n', payload_string)))
+ obj.payload_bytes = payload_string_crlf.encode(charset)
obj.filename = part.get_filename()
obj.content_type = part.get_content_type()
+ obj.content_disposition = part['content-disposition']
obj.description_list = part['content-description']
- # your guess is as good as a-myy-ee-ine...
- if obj.charset == None:
- obj.charset = 'utf-8'
-
- if obj.payload_bytes != None:
+ if mime_decoded_bytes != None:
try:
payload = PayloadPiece()
- payload.string = obj.payload_bytes.decode(obj.charset)
- payload.piece_type = 'text'
+ payload.string = mime_decoded_bytes.decode(charset)
+ payload.piece_type = TxtType.text
obj.payload_pieces = [payload]
except UnicodeDecodeError:
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:
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:
for piece in eddymsg_obj.payload_pieces:
- if piece.piece_type == "text":
+ if piece.piece_type == TxtType.text:
# don't transform the plaintext.
pass
- elif piece.piece_type == "message":
- (plaintext, sigs) = decrypt_block(piece.string, gpgme_ctx)
+ elif piece.piece_type == TxtType.message:
+ piece.gpg_data = GPGData()
+
+ (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.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
+ piece.gpg_data.plainobj = parse_pgp_mime(plaintext_b, gpgme_ctx)
continue
# if not encrypted, check to see if this is an armored signature.
- (plaintext, sigs) = verify_sig_message(piece.string, gpgme_ctx)
+ (plaintext_b, sigs, sigkey_missing, key_cannot_encrypt) = verify_sig_message(piece.string, gpgme_ctx)
+
+ 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.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()
+
+ (key_fps, key_cannot_encrypt) = add_gpg_key(piece.string, gpgme_ctx)
- # FIXME: handle pubkeys first, so that signatures can be validated
- # on freshly imported keys
- elif piece.piece_type == "pubkey":
- key_fps = add_gpg_key(piece.string, gpgme_ctx)
+ piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
if key_fps != []:
- piece.gpg_data = GPGData()
piece.gpg_data.keys = key_fps
- elif piece.piece_type == "clearsign":
- (plaintext, sig_fps) = verify_clear_signature(piece.string, gpgme_ctx)
+ elif piece.piece_type == TxtType.detachedsig:
+ piece.gpg_data = GPGData()
- 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 == "detachedsig":
for prev in prev_parts:
- payload_bytes = prev.payload_bytes
- sig_fps = verify_detached_signature(piece.string, payload_bytes, gpgme_ctx)
+ (sig_fps, sigkey_missing, key_cannot_encrypt) = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx)
+
+ piece.gpg_data.sigkey_missing = sigkey_missing
+ piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
if sig_fps != []:
- piece.gpg_data = GPGData()
piece.gpg_data.sigs = sig_fps
piece.gpg_data.plainobj = prev
break
"""
for piece in eddymsg_obj.payload_pieces:
- if piece.piece_type == "text":
+ if piece.piece_type == TxtType.text:
# don't quote the plaintext part.
pass
- elif piece.piece_type == "message":
+ elif piece.piece_type == TxtType.message:
prepare_for_reply_message(piece, replyinfo_obj)
- elif piece.piece_type == "pubkey":
+ elif piece.piece_type == TxtType.pubkey:
prepare_for_reply_pubkey(piece, replyinfo_obj)
- elif (piece.piece_type == "clearsign") \
- or (piece.piece_type == "detachedsig") \
- or (piece.piece_type == "signature"):
+ elif (piece.piece_type == TxtType.detachedsig) \
+ or (piece.piece_type == TxtType.signature):
prepare_for_reply_sig(piece, replyinfo_obj)
"""Helper function for prepare_for_reply()
This function is called when the piece_type of a payload piece is
- "message", or GPG Message block. This should be encrypted text. If the
- encryted block is signed, a sig will be attached to .target_key unless
- there is already one there.
+ TxtType.message, or GPG Message block. This should be encrypted text. If
+ the encryted block is correclty signed, a sig will be attached to
+ .target_key unless there is already one there.
Args:
piece: a PayloadPiece object.
replyinfo_obj: object which gets updated with decryption status, etc.
-
Returns:
Nothing
Pre:
- the piece.payload_piece value should be "message".
+ the piece.payload_piece value should be TxtType.message.
Post:
- replyinfo_obj gets updated with decryption status, signing status and a
- potential signing key.
+ replyinfo_obj gets updated with decryption status, signing status, a
+ potential signing key, posession status of the public key for the
+ signature and encryption capability status if that key is missing.
"""
- if piece.gpg_data == None:
- replyinfo_obj.failed_decrypt = True
+ if piece.gpg_data.plainobj == None:
+ replyinfo_obj.decrypt_failure = True
return
- replyinfo_obj.success_decrypt = True
+ replyinfo_obj.decrypt_success = True
# we already have a key (and a message)
if replyinfo_obj.target_key != None:
return
- if piece.gpg_data.sigs != []:
- replyinfo_obj.target_key = piece.gpg_data.sigs[0]
- get_signed_part = False
- else:
+ if piece.gpg_data.sigs == []:
+ if piece.gpg_data.sigkey_missing == True:
+ replyinfo_obj.sigkey_missing = True
+
+ if piece.gpg_data.key_cannot_encrypt == True:
+ replyinfo_obj.key_cannot_encrypt = True
+
# only include a signed message in the reply.
get_signed_part = True
- replyinfo_obj.msg_to_quote = flatten_decrypted_payloads(piece.gpg_data.plainobj, get_signed_part)
+ else:
+ replyinfo_obj.target_key = piece.gpg_data.sigs[0]
+ replyinfo_obj.sig_success = True
+ get_signed_part = False
+
+ flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, get_signed_part)
# to catch public keys in encrypted blocks
prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
replyinfo_obj: a ReplyInfo object
Pre:
- piece.piece_type should be set to "pubkey".
+ piece.piece_type should be set to TxtType.pubkey .
Post:
replyinfo_obj has its fields updated.
"""
- if piece.gpg_data == None or piece.gpg_data.keys == []:
- replyinfo_obj.no_public_key = True
+ if piece.gpg_data.keys == []:
+ if piece.gpg_data.key_cannot_encrypt == True:
+ replyinfo_obj.key_cannot_encrypt = True
else:
- replyinfo_obj.public_key_received = True
+ replyinfo_obj.pubkey_success = True
- if replyinfo_obj.fallback_target_key == None:
- replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
+ # prefer public key as a fallback for the encrypted reply
+ replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
def prepare_for_reply_sig (piece, replyinfo_obj):
replyinfo_obj: a ReplyInfo object
Pre:
- piece.piece_type should be set to "clearsign", "signature", or
- "detachedsig".
+ piece.piece_type should be set to TxtType.signature, or
+ TxtType.detachedsig .
Post:
replyinfo_obj has its fields updated.
"""
- if piece.gpg_data == None or piece.gpg_data.sigs == []:
+ if piece.gpg_data.sigs == []:
replyinfo_obj.sig_failure = True
+
+ if piece.gpg_data.sigkey_missing == True:
+ replyinfo_obj.sigkey_missing = True
+
+ if piece.gpg_data.key_cannot_encrypt == True:
+ replyinfo_obj.key_cannot_encrypt = True
+
else:
replyinfo_obj.sig_success = True
if replyinfo_obj.fallback_target_key == None:
replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
+ if (piece.piece_type == TxtType.signature):
+ # to catch public keys in signature blocks
+ prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
-def flatten_decrypted_payloads (eddymsg_obj, get_signed_part):
- """Returns 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.
- Returns the string representation of the first signed/encrypted or signed
- then encrypted block of text. (Signature inside of Encryption)
+ When given a decrypted payload, it will add either the plaintext or signed
+ plaintext to the reply message, depeding on 'get_signed_part'. This is
+ useful for ensuring that the reply message only comes from a signed and
+ ecrypted GPG message. It also sets the target_key for encrypting the reply
+ if it's told to get signed text only.
Args:
eddymsg_obj: the message in EddyMsg format created by decrypting GPG
text
+ replyinfo_obj: a ReplyInfo object for holding the message to quote and
+ the target_key to encrypt to.
get_signed_part: True if we should only include text that contains a
further signature. If False, then include plain text.
Returns:
- A string representation of encrypted and signed text.
+ Nothing
Pre:
The EddyMsg instance passed in should be a piece.gpg_data.plainobj
which represents decrypted text. It may or may not be signed on that
level.
- """
- flat_string = ""
+ Post:
+ the ReplyInfo instance may have a new 'target_key' set and its
+ 'msg_to_quote' will be updated with (possibly signed) plaintext, if any
+ could be found.
+ """
if eddymsg_obj == None:
- return ""
+ return
# recurse on multi-part mime
if eddymsg_obj.multipart == True:
for sub in eddymsg_obj.subparts:
- flat_string += flatten_decrypted_payloads (sub, get_signed_part)
-
- return flat_string
+ flatten_decrypted_payloads(sub, replyinfo_obj, get_signed_part)
for piece in eddymsg_obj.payload_pieces:
if (get_signed_part):
- if ((piece.piece_type == "clearsign") \
- or (piece.piece_type == "detachedsig") \
- or (piece.piece_type == "signature")) \
- and (piece.gpg_data != None):
- # FIXME: the key used to sign this message needs to be the one that is used for the encrypted reply.
- flat_string += flatten_decrypted_payloads (piece.gpg_data.plainobj, False)
+ if ((piece.piece_type == TxtType.detachedsig) \
+ or (piece.piece_type == TxtType.signature)) \
+ and (piece.gpg_data != None) \
+ and (piece.gpg_data.plainobj != None):
+ flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, False)
+ replyinfo_obj.target_key = piece.gpg_data.sigs[0]
break
else:
- if piece.piece_type == "text":
- flat_string += piece.string
-
- return flat_string
+ if (eddymsg_obj.content_disposition == None \
+ or not eddymsg_obj.content_disposition.startswith("attachment")) \
+ and piece.piece_type == TxtType.text:
+ replyinfo_obj.msg_to_quote += piece.string
def get_key_from_fp (replyinfo_obj, gpgme_ctx):
"""Obtains a public key object from a key fingerprint
- If 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:
- 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
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
-
- if replyinfo_obj.target_key != None:
- try:
- encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key)
- return encrypt_to_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)
- 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):
reply_plain = ""
- if replyinfo_obj.success_decrypt == True:
- reply_plain += replyinfo_obj.replies['success_decrypt']
+ if (replyinfo_obj.pubkey_success == True):
+ reply_plain += replyinfo_obj.replies['greeting']
+ reply_plain += "\n\n"
- if replyinfo_obj.no_public_key == False:
- quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
- reply_plain += quoted_text
- elif replyinfo_obj.failed_decrypt == True:
+ if replyinfo_obj.decrypt_success == True:
+ debug('decrypt success')
+ reply_plain += replyinfo_obj.replies['success_decrypt']
+ reply_plain += "\n\n"
+
+ elif replyinfo_obj.decrypt_failure == True:
+ debug('decrypt failure')
reply_plain += replyinfo_obj.replies['failed_decrypt']
+ reply_plain += "\n\n"
if replyinfo_obj.sig_success == True:
- reply_plain += "\n\n"
+ debug('signature success')
reply_plain += replyinfo_obj.replies['sig_success']
+ reply_plain += "\n\n"
elif replyinfo_obj.sig_failure == True:
- reply_plain += "\n\n"
+ debug('signature failure')
reply_plain += replyinfo_obj.replies['sig_failure']
+ reply_plain += "\n\n"
- if replyinfo_obj.public_key_received == True:
- reply_plain += "\n\n"
+ if (replyinfo_obj.pubkey_success == True):
+ debug('public key received')
reply_plain += replyinfo_obj.replies['public_key_received']
+ reply_plain += "\n\n"
- elif replyinfo_obj.no_public_key == True:
+ elif (replyinfo_obj.sigkey_missing == True):
+ debug('no public key')
+ reply_plain += replyinfo_obj.replies['no_public_key']
reply_plain += "\n\n"
+
+ elif (replyinfo_obj.key_can_encrypt == False) \
+ and (replyinfo_obj.key_cannot_encrypt == True):
+ debug('bad public key')
reply_plain += replyinfo_obj.replies['no_public_key']
+ reply_plain += "\n\n"
+
+
+ if (replyinfo_obj.decrypt_success == True) \
+ and (replyinfo_obj.sig_success == True) \
+ and (replyinfo_obj.have_reply_key == True):
+ debug('message quoted')
+ reply_plain += replyinfo_obj.replies['quote_follows']
+ reply_plain += "\n\n"
+ quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
+ reply_plain += quoted_text
+ reply_plain += "\n\n"
+
+
+ if (reply_plain == ""):
+ debug('plaintext message')
+ reply_plain += replyinfo_obj.replies['failed_decrypt']
+ reply_plain += "\n\n"
- reply_plain += "\n\n"
reply_plain += replyinfo_obj.replies['signature']
+ reply_plain += "\n\n"
return reply_plain
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'))
- result = gpgme_ctx.import_(fp)
- imports = result.imports
+ try:
+ result = gpgme_ctx.import_(fp)
+ imports = result.imports
+ except gpgme.GpgmeError:
+ imports = []
key_fingerprints = []
+ key_cannot_encrypt = False
+
+ for import_res in imports:
+ fingerprint = import_res[0]
+
+ try:
+ key_obj = gpgme_ctx.get_key(fingerprint)
+ except:
+ key_obj = None
- if imports != []:
- for import_ in imports:
- fingerprint = import_[0]
+ if key_obj != None and is_key_usable(key_obj):
key_fingerprints += [fingerprint]
+ key_cannot_encrypt = False
debug("added gpg key: " + fingerprint)
- return key_fingerprints
+ elif key_fingerprints == []:
+ key_cannot_encrypt = True
+
+ return (key_fingerprints, key_cannot_encrypt)
def verify_sig_message (msg_block, gpgme_ctx):
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'))
try:
sigs = gpgme_ctx.verify(block_b, None, plain_b)
- except:
- return ("",[])
-
- plaintext = plain_b.getvalue().decode('utf-8')
-
- fingerprints = []
- for sig in sigs:
- fingerprints += [sig.fpr]
- return (plaintext, fingerprints)
-
-
-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
+ except gpgme.GpgmeError:
+ return ("",[],False,False)
- 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()
-
- result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
+ plaintext_b = plain_b.getvalue()
- # FIXME: this might require using the charset of the mime part.
- plaintext = ptxt_fp.getvalue().decode('utf-8')
+ (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx)
- 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):
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)
- ptxt_fp = io.BytesIO()
- result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
+ try:
+ sigs = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
+ except gpgme.GpgmeError:
+ return ([],False,False)
- sig_fingerprints = []
- for res_ in result:
- sig_fingerprints += [res_.fpr]
+ (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx)
- return sig_fingerprints
+ return (fingerprints, sigkey_missing, key_cannot_encrypt)
def decrypt_block (msg_block, gpgme_ctx):
- """Decrypts a block of GPG text, and verifies any included sigatures.
+ """Decrypts a block of GPG text and verifies any included sigatures.
Some encypted messages have embeded signatures, so those are verified too.
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'))
try:
sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
- except:
- return ("",[])
+ except gpgme.GpgmeError:
+ return ("",[],False,False)
- plaintext = plain_b.getvalue().decode('utf-8')
+ 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
+
+ Returns:
+ fingerprints: a list of fingerprints
+ sigkey_missing: a boolean marking whether public keys are missing for
+ all available signatures.
+ key_cannot_encrypt: a boolearn marking whether available public keys are
+ incapable of encryption.
+ """
+
+ sigkey_missing = False
+ key_cannot_encrypt = False
fingerprints = []
+
for sig in sigs:
- fingerprints += [sig.fpr]
- return (plaintext, fingerprints)
+ if (sig.summary == 0) or (sig.summary & gpgme.SIGSUM_VALID != 0) or (sig.summary & gpgme.SIGSUM_GREEN != 0):
+ try:
+ key_obj = gpgme_ctx.get_key(sig.fpr)
+ except:
+ if fingerprints == []:
+ sigkey_missing = True
+ continue
+
+ if is_key_usable(key_obj):
+ fingerprints += [sig.fpr]
+ key_cannot_encrypt = False
+ sigkey_missing = False
+
+ elif fingerprints == []:
+ key_cannot_encrypt = True
+
+ 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
-def email_to_from_subject (email_text):
+ Returns this information from an email.
+
+ Args:
+ email_bytes: the byte string form of the email
+
+ Returns:
+ the email To:, Reply-To: (or From:), and Subject: fields as strings
+ """
- email_struct = email.parser.Parser().parsestr(email_text)
+ email_struct = email.parser.BytesHeaderParser().parsebytes(email_bytes)
email_to = email_struct['To']
email_from = email_struct['From']
+ email_reply_to = email_struct['Reply-To']
+
email_subject = email_struct['Subject']
- return email_to, email_from, email_subject
+ if email_reply_to == None:
+ email_reply_to = email_from
+
+ return email_to, email_reply_to, email_subject
+
+
+def import_lang_pick_address(email_to, hostname):
+ """Imports language file for i18n support; makes reply from address
+
+ The language imported depends on the To: address of the email received by
+ edward. an -en ending implies the English language, whereas a -ja ending
+ implies Japanese. The list of supported languages is listed in the 'langs'
+ list at the beginning of the program. This function also chooses the
+ language-dependent address which can be used as the From address in the
+ reply email.
+
+ Args:
+ email_to: the string containing the email address that the mail was
+ sent to.
+ hostname: the hostname part of the reply email's from address
+ Returns:
+ the reference to the imported language module. The only variable in
+ this file is the 'replies' dictionary.
+ """
-def import_lang(email_to):
+ # default
+ use_lang = "en"
if email_to != None:
for lang in langs:
if "edward-" + lang in email_to:
- lang = "lang." + re.sub('-', '_', lang)
- language = importlib.import_module(lang)
+ use_lang = lang
+ break
+
+ lang_mod_name = "lang." + re.sub('-', '_', use_lang)
+ lang_module = importlib.import_module(lang_mod_name)
+
+ reply_from = "edward-" + use_lang + "@" + hostname
- return language
+ return lang_module, reply_from
- return importlib.import_module("lang.en")
+def generate_encrypted_mime (plaintext, email_to, email_from, email_subject,
+ encrypt_to_key, gpgme_ctx):
+ """This function creates the mime email reply. It can encrypt the email.
-def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
- gpgme_ctx):
+ If the encrypt_key is included, then the email is encrypted and signed.
+ Otherwise it is unencrypted.
+
+ Args:
+ plaintext: the plaintext body of the message to create.
+ email_to: the email address to reply to
+ email_subject: the subject to use in reply
+ encrypt_to_key: the key object to use for encrypting the email. (or
+ None)
+ gpgme_ctx: the gpgme context
- # 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
+ Returns
+ A string version of the mime message, possibly encrypted and signed.
+ """
- # MIMEText doesn't allow setting the text encoding
- # so we use MIMENonMultipart.
- plaintext_mime = MIMENonMultipart('text', 'plain')
- plaintext_mime.set_payload(plaintext, charset=char_set)
+ plaintext_mime = MIMEText(plaintext)
+ plaintext_mime.set_charset('utf-8')
if (encrypt_to_key != None):
else:
message_mime = plaintext_mime
- message_mime['To'] = email_from
+ message_mime['Auto-Submitted'] = 'auto-replied'
+
+ message_mime['To'] = email_to
+ message_mime['From'] = email_from
message_mime['Subject'] = email_subject
reply = message_mime.as_string()
return reply
+def send_reply(email_txt, reply_to, reply_from):
+ """Sends reply email
+
+ Sent to original sender
+
+ Args:
+ email_txt: message as a string
+ reply_to: recipient of reply
+ reply_from: edward's specific email address
+
+ Post:
+ Email is sent
+ """
+
+ if reply_to == None:
+ error("*** ERROR: No one to send email to.")
+ exit(1)
+
+ s = smtplib.SMTP('localhost')
+ s.sendmail(reply_from, reply_to, email_txt)
+ s.quit()
+
+
def email_quote_text (text):
+ """Quotes input text by inserting "> "s
+
+ This is useful for quoting a text for the reply message. It inserts "> "
+ strings at the beginning of lines.
+
+ Args:
+ text: plain text to quote
+
+ Returns:
+ Quoted text
+ """
quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
+ """Encrypts and signs plaintext
+
+ This encrypts and signs a message.
+
+ Args:
+ plaintext: text to sign and ecrypt
+ encrypt_to_key: the key object to encrypt to
+ gpgme_ctx: the gpgme context
+ Returns:
+ An encrypted and signed string of text
+ """
+
+ # the plaintext should be mime encoded in an ascii-compatible form
plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
encrypted_bytes = io.BytesIO()
def error (error_msg):
+ """Write an error message to stdout
+
+ The error message includes the program name.
+
+ Args:
+ error_msg: the message to print
+
+ Returns:
+ Nothing
+
+ Post:
+ An error message is printed to stdout
+ """
sys.stderr.write(progname + ": " + str(error_msg) + "\n")
def debug (debug_msg):
+ """Writes a debug message to stdout if debug == True
+
+ If the debug option is set in edward_config.py, then the passed message
+ gets printed to stdout.
+
+ Args:
+ debug_msg: the message to print to stdout
+
+ Returns:
+ Nothing
+
+ Post:
+ A debug message is printed to stdout
+ """
if edward_config.debug == True:
error(debug_msg)
def handle_args ():
+ """Sets the progname variable and processes optional argument
+
+ If there are more than two arguments then edward complains and quits. An
+ single "-p" argument sets the print_reply_only option, which makes edward
+ print email replies instead of mailing them.
+
+ Args:
+ None
+
+ Returns:
+ True if edward should print arguments instead of mailing them,
+ otherwise it returns False.
+
+ Post:
+ Exits with error 1 if there are more than two arguments, otherwise
+ returns the print_reply_only option.
+ """
global progname
progname = sys.argv[0]
- if len(sys.argv) > 1:
- print(progname + ": error, this program doesn't " \
- "need any arguments.", file=sys.stderr)
+ print_reply_only = False
+
+ if len(sys.argv) > 2:
+ print(progname + " usage: " + progname + " [-p]\n\n" \
+ + " -p print reply message to stdout, do not mail it\n", \
+ file=sys.stderr)
exit(1)
+ elif (len(sys.argv) == 2) and (sys.argv[1] == "-p"):
+ print_reply_only = True
+
+ return print_reply_only
+
if __name__ == "__main__":
+ """Executes main if this file is not loaded interactively"""
main()