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.tgz
* 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 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
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"]
"""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.
'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()
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)
+ email_bytes = sys.stdin.buffer.read()
+ 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_from, email_subject = email_to_from_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,
+ 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_subject, email_from, reply_from)
def get_gpg_context (gnupghome, sign_with_key_fp):
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)
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
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
a single-part EddyMsg() object
"""
- charset = part.get_content_charset()
- mime_decoded_bytes = part.get_payload(decode=True)
-
obj = EddyMsg()
- obj.payload_bytes = part.as_bytes()
- obj.filename = part.get_filename()
- obj.content_type = part.get_content_type()
- obj.description_list = part['content-description']
+ 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:
+ obj.payload_bytes = payload_string.encode(charset)
+
+ obj.filename = part.get_filename()
+ obj.content_type = part.get_content_type()
+ obj.description_list = part['content-description']
+
if mime_decoded_bytes != None:
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:
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)
- # FIXME: consider handling 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)
+ elif piece.piece_type == TxtType.pubkey:
+ piece.gpg_data = GPGData()
+
+ (key_fps, key_cannot_encrypt) = 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 == "detachedsig":
+ elif piece.piece_type == TxtType.detachedsig:
+ piece.gpg_data = GPGData()
+
for prev in prev_parts:
- sig_fps = verify_detached_signature(piece.string, prev.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 == "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
+ 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
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
# prefer public key as a fallback for the encrypted reply
replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
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.
"""
- 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, replyinfo_obj, get_signed_part):
"""For creating a string representation of a signed, encrypted part.
for piece in eddymsg_obj.payload_pieces:
if (get_signed_part):
- if ((piece.piece_type == "detachedsig") \
- or (piece.piece_type == "signature")) \
- and (piece.gpg_data != None):
+ 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":
+ if 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
+ 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 gpgme.GpgmeError:
- 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:
+ if (replyinfo_obj.pubkey_success == True):
+ reply_plain += replyinfo_obj.replies['greeting']
+ reply_plain += "\n\n"
+
+
+ if replyinfo_obj.decrypt_success == True:
+ debug('decrypt success')
reply_plain += replyinfo_obj.replies['success_decrypt']
- if replyinfo_obj.no_public_key == False:
+ 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
- elif replyinfo_obj.failed_decrypt == True:
+ 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 (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'))
imports = []
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:
+ pass
+
+ if 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 gpgme.GpgmeError:
- return ("",[])
+ return ("",[],False,False)
- plaintext = plain_b.getvalue().decode('utf-8')
+ plaintext_b = plain_b.getvalue()
- fingerprints = []
- for sig in sigs:
- fingerprints += [sig.fpr]
- return (plaintext, fingerprints)
+ (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx)
+
+ 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)
try:
- result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
+ sigs = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
except gpgme.GpgmeError:
- return []
+ 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 gpgme.GpgmeError:
- return ("",[])
+ 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
-def email_to_from_subject (email_text):
+ 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 email_to_from_subject (email_bytes):
"""Returns the values of the email's To:, From: and Subject: fields
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:
the email To:, From:, and Subject: fields as strings
"""
- email_struct = email.parser.Parser().parsestr(email_text)
+ email_struct = email.parser.BytesParser().parsebytes(email_bytes)
email_to = email_struct['To']
email_from = email_struct['From']
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
-
- # 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)
- 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 = plaintext_mime
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"""