import types
try:
+ from email import Message
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.utils import getaddress
except ImportError:
# adjust to old python 2.4
+ from email import Message
from email.MIMEText import MIMEText
from email.MIMEMultipart import MIMEMultipart
from email.MIMENonMultipart import MIMENonMultipart
from email.Generator import Generator
from email.parser import Parser
from email.Utils import getaddresses
-
+
getaddress = getaddresses
class MIMEApplication (MIMENonMultipart):
def __init__(self, _data, _subtype, _encoder, **params):
details. Alternatively, you can send your passphrase in on stdin
echo 'passphrase' | %prog [options]
or use the --passphrase-file option
- %prog [options] --passphrase-file FILE [more options]
+ %prog [options] --passphrase-file FILE [more options]
Both of these alternatives are much less secure than gpg-agent. You
have been warned.
"""
PASSPHRASE = None
# The following commands are adapted from my .mutt/pgp configuration
-#
+#
# Printf-like sequences:
# %a The value of PGP_SIGN_AS.
# %f Expands to the name of a file with text to be signed/encrypted.
# strings.
# %r One key ID (e.g. recipient email address) to build a
# pgp_reciepient_arg string.
-#
+#
# The above sequences can be used to optionally print a string if
# their length is nonzero. For example, you may only want to pass the
# -u/--local-user argument to gpg if PGP_SIGN_AS is defined. To
pgp_encrypt_sign_command='/usr/bin/gpg --no-verbose --quiet --batch %p --output - --encrypt --sign %?a?-u "%a"? --armor --textmode --always-trust --encrypt-to "%a" %R -- %f'
sendmail='/usr/sbin/sendmail -t'
+def mail(msg, sendmail=None):
+ """
+ Send an email Message instance on its merry way.
+
+ We can shell out to the user specified sendmail in case
+ the local host doesn't have an SMTP server set up
+ for easy smtplib usage.
+ """
+ if sendmail != None:
+ execute(sendmail, stdin=flatten(msg))
+ return None
+ s = smtplib.SMTP()
+ s.connect()
+ s.sendmail(from_addr=source_email(msg),
+ to_addrs=target_emails(msg),
+ msg=flatten(msg))
+ s.close()
+
+def header_from_text(text, encoding="us-ascii"):
+ """
+ Simple wrapper for instantiating an email.Message from text.
+ >>> header = header_from_text('\\n'.join(['From: me@big.edu','To: you@big.edu','Subject: testing']))
+ >>> print flatten(header)
+ From: me@big.edu
+ To: you@big.edu
+ Subject: testing
+ <BLANKLINE>
+ <BLANKLINE>
+ """
+ text = text.strip()
+ if type(text) == types.UnicodeType:
+ text = text.encode(encoding)
+ # assume StringType arguments are already encoded
+ p = Parser()
+ return p.parsestr(text, headersonly=True)
+
+def guess_encoding(text):
+ if type(text) == types.StringType:
+ encoding = "us-ascii"
+ elif type(text) == types.UnicodeType:
+ for encoding in ["us-ascii", "iso-8859-1", "utf-8"]:
+ try:
+ text.encode(encoding)
+ except UnicodeError:
+ pass
+ else:
+ break
+ assert encoding != None
+ return encoding
+
+def encodedMIMEText(body, encoding=None):
+ if encoding == None:
+ encoding = guess_encoding(body)
+ if encoding == "us-ascii":
+ return MIMEText(body)
+ else:
+ # Create the message ('plain' stands for Content-Type: text/plain)
+ return MIMEText(body.encode(encoding), 'plain', encoding)
+
+def append_text(text_part, new_text):
+ original_payload = text_part.get_payload(decode=True)
+ new_payload = u"%s%s" % (original_payload, new_text)
+ new_encoding = guess_encoding(new_payload)
+ text_part.set_payload(new_payload.encode(new_encoding), new_encoding)
+
+def attach_root(header, root_part):
+ """
+ Attach the email.Message root_part to the email.Message header
+ without generating a multi-part message.
+ """
+ for k,v in header.items():
+ root_part[k] = v
+ return root_part
+
def execute(args, stdin=None, expect=(0,)):
"""
Execute a command (allows us to drive gpg).
"""
if replacement_text == None:
replacement_text = ""
- regexp = re.compile('%[?]'+format_char+'[?]([^?]*)[?]')
+ regexp = re.compile('%[?]'+format_char+'[?]([^?]*)[?]')
if len(replacement_text) > 0:
str = regexp.sub('\g<1>', template)
else:
str = regexp.sub(replacement_text, str)
return str
-def flatten(msg):
+def flatten(msg, to_unicode=False):
"""
Produce flat text output from an email Message instance.
"""
g = Generator(fp, mangle_from_=False)
g.flatten(msg)
text = fp.getvalue()
- encoding = msg.get_content_charset()
- return unicode(text, encoding=encoding)
+ if to_unicode == True:
+ encoding = msg.get_content_charset() or "utf-8"
+ text = unicode(text, encoding=encoding)
+ return text
def source_email(msg, return_realname=False):
"""
resent_tos = msg.get_all('resent-to', [])
resent_ccs = msg.get_all('resent-cc', [])
resent_bccs = msg.get_all('resent-bcc', [])
- all_recipients = getaddresses(tos + ccs + bccs + resent_tos + resent_ccs + resent_bccs)
+ all_recipients = getaddresses(tos + ccs + bccs + resent_tos
+ + resent_ccs + resent_bccs)
return [addr[1] for addr in all_recipients]
-def mail(msg, sendmail=None):
- """
- Send an email Message instance on its merry way.
-
- We can shell out to the user specified sendmail in case
- the local host doesn't have an SMTP server set up
- for easy smtplib usage.
- """
- if sendmail != None:
- execute(sendmail, stdin=flatten(msg))
- return None
- s = smtplib.SMTP()
- s.connect()
- s.sendmail(from_addr=source_email(msg),
- to_addrs=target_emails(msg),
- msg=flatten(msg))
- s.close()
-
-class Mail (object):
+class PGPMimeMessageFactory (object):
"""
See http://www.ietf.org/rfc/rfc3156.txt for specification details.
- >>> m = Mail('\\n'.join(['From: me@big.edu','To: you@big.edu','Subject: testing']), 'check 1 2\\ncheck 1 2\\n')
- >>> print m.sourceEmail()
- me@big.edu
- >>> print m.targetEmails()
- ['you@big.edu']
+ >>> from_addr = "me@big.edu"
+ >>> to_addr = "you@you.edu"
+ >>> header = header_from_text('\\n'.join(['From: %s'%from_addr,'To: %s'%to_addr,'Subject: testing']))
+ >>> source_email(header) == from_addr
+ True
+ >>> target_emails(header) == [to_addr]
+ True
+ >>> m = PGPMimeMessageFactory('check 1 2\\ncheck 1 2\\n')
>>> print flatten(m.clearBodyPart())
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
- From: me@big.edu
- To: you@big.edu
- Subject: testing
<BLANKLINE>
check 1 2
check 1 2
<BLANKLINE>
- >>> m.sign()
+ >>> signed = m.sign(header)
>>> signed.set_boundary('boundsep')
- >>> print m.stripSig(flatten(signed)).replace('\\t', ' '*4)
- Content-Type: multipart/signed;
- protocol="application/pgp-signature";
+ >>> print flatten(signed).replace('\\t', ' '*4) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+ Content-Type: multipart/signed; protocol="application/pgp-signature";
micalg="pgp-sha1"; boundary="boundsep"
MIME-Version: 1.0
- From: me@big.edu
- To: you@big.edu
- Subject: testing
Content-Disposition: inline
<BLANKLINE>
--boundsep
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
- Content-Type: text/plain
Content-Disposition: inline
<BLANKLINE>
check 1 2
charset="us-ascii"
<BLANKLINE>
-----BEGIN PGP SIGNATURE-----
- SIGNATURE STRIPPED (depends on current time)
+ ...
-----END PGP SIGNATURE-----
<BLANKLINE>
--boundsep--
- >>> encrypted = m.encrypt()
+ >>> encrypted = m.encrypt(header)
>>> encrypted.set_boundary('boundsep')
- >>> print m.stripPGP(flatten(encrypted)).replace('\\t', ' '*4)
+ >>> print flatten(encrypted).replace('\\t', ' '*4) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
Content-Type: multipart/encrypted;
protocol="application/pgp-encrypted";
micalg="pgp-sha1"; boundary="boundsep"
MIME-Version: 1.0
- From: me@big.edu
- To: you@big.edu
- Subject: testing
Content-Disposition: inline
<BLANKLINE>
--boundsep
Content-Type: application/octet-stream; charset="us-ascii"
<BLANKLINE>
-----BEGIN PGP MESSAGE-----
- MESSAGE STRIPPED (depends on current time)
+ ...
-----END PGP MESSAGE-----
<BLANKLINE>
--boundsep--
- >>> signedAndEncrypted = m.signAndEncrypt()
+ >>> signedAndEncrypted = m.signAndEncrypt(header)
>>> signedAndEncrypted.set_boundary('boundsep')
- >>> print m.stripPGP(flatten(signedAndEncrypted)).replace('\\t', ' '*4)
+ >>> print flatten(signedAndEncrypted).replace('\\t', ' '*4) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
Content-Type: multipart/encrypted;
protocol="application/pgp-encrypted";
micalg="pgp-sha1"; boundary="boundsep"
MIME-Version: 1.0
- From: me@big.edu
- To: you@big.edu
- Subject: testing
Content-Disposition: inline
<BLANKLINE>
--boundsep
Content-Type: application/octet-stream; charset="us-ascii"
<BLANKLINE>
-----BEGIN PGP MESSAGE-----
- MESSAGE STRIPPED (depends on current time)
+ ...
-----END PGP MESSAGE-----
<BLANKLINE>
--boundsep--
"""
- def __init__(self, header, body):
- self.header = header.strip()
+ def __init__(self, body):
self.body = body
- if type(self.header) == types.UnicodeType:
- self.header = self.header.encode("ascii")
- p = Parser()
- self.headermsg = p.parsestr(self.header, headersonly=True)
- def sourceEmail(self):
- return source_email(self.headermsg)
- def targetEmails(self):
- return target_emails(self.headermsg)
- def encodedMIMEText(self, body, encoding=None):
- if encoding == None:
- if type(body) == types.StringType:
- encoding = "US-ASCII"
- elif type(body) == types.UnicodeType:
- for encoding in ["US-ASCII", "ISO-8859-1", "UTF-8"]:
- try:
- body.encode(encoding)
- except UnicodeError:
- pass
- else:
- break
- assert encoding != None
- # Create the message ('plain' stands for Content-Type: text/plain)
- if encoding == "US-ASCII":
- return MIMEText(body)
- else:
- return MIMEText(body.encode(encoding), 'plain', encoding)
def clearBodyPart(self):
- body = self.encodedMIMEText(self.body)
+ body = encodedMIMEText(self.body)
body.add_header('Content-Disposition', 'inline')
return body
def passphrase_arg(self, passphrase=None):
def plain(self):
"""
text/plain
- """
- msg = self.encodedMIMEText(self.body)
- for k,v in self.headermsg.items():
- msg[k] = v
- return msg
- def sign(self, passphrase=None):
+ """
+ return encodedMIMEText(self.body)
+ def sign(self, header, passphrase=None):
"""
multipart/signed
+-> text/plain (body)
+-> application/pgp-signature (signature)
- """
+ """
passphrase,pass_arg = self.passphrase_arg(passphrase)
body = self.clearBodyPart()
bfile = tempfile.NamedTemporaryFile()
args = replace(pgp_sign_command, 'f', bfile.name)
if PGP_SIGN_AS == None:
- pgp_sign_as = '<%s>' % self.sourceEmail()
+ pgp_sign_as = '<%s>' % source_email(header)
else:
pgp_sign_as = PGP_SIGN_AS
args = replace(args, 'a', pgp_sign_as)
args = replace(args, 'p', pass_arg)
status,output,error = execute(args, stdin=passphrase)
signature = output
-
- sig = MIMEApplication(_data=signature, _subtype='pgp-signature; name="signature.asc"', _encoder=encode_7or8bit)
+
+ sig = MIMEApplication(_data=signature,
+ _subtype='pgp-signature; name="signature.asc"',
+ _encoder=encode_7or8bit)
sig['Content-Description'] = 'signature'
sig.set_charset('us-ascii')
-
- msg = MIMEMultipart('signed', micalg='pgp-sha1', protocol='application/pgp-signature')
+
+ msg = MIMEMultipart('signed', micalg='pgp-sha1',
+ protocol='application/pgp-signature')
msg.attach(body)
msg.attach(sig)
-
- for k,v in self.headermsg.items():
- msg[k] = v
+
msg['Content-Disposition'] = 'inline'
return msg
- def encrypt(self, passphrase=None):
+ def encrypt(self, header, passphrase=None):
"""
multipart/encrypted
+-> application/pgp-encrypted (control information)
bfile = tempfile.NamedTemporaryFile()
bfile.write(flatten(body))
bfile.flush()
-
- recipient_string = ' '.join([replace(pgp_recipient_arg, 'r', recipient) for recipient in self.targetEmails()])
+
+ recipients = [replace(pgp_recipient_arg, 'r', recipient)
+ for recipient in target_emails(header)]
+ recipient_string = ' '.join(recipients)
args = replace(pgp_encrypt_only_command, 'R', recipient_string)
args = replace(args, 'f', bfile.name)
if PGP_SIGN_AS == None:
- pgp_sign_as = '<%s>' % self.sourceEmail()
+ pgp_sign_as = '<%s>' % source_email(header)
else:
pgp_sign_as = PGP_SIGN_AS
args = replace(args, 'a', pgp_sign_as)
status,output,error = execute(args)
encrypted = output
-
- enc = MIMEApplication(_data=encrypted, _subtype='octet-stream', _encoder=encode_7or8bit)
+
+ enc = MIMEApplication(_data=encrypted, _subtype='octet-stream',
+ _encoder=encode_7or8bit)
enc.set_charset('us-ascii')
-
- control = MIMEApplication(_data='Version: 1\n', _subtype='pgp-encrypted', _encoder=encode_7or8bit)
-
- msg = MIMEMultipart('encrypted', micalg='pgp-sha1', protocol='application/pgp-encrypted')
+
+ control = MIMEApplication(_data='Version: 1\n', _subtype='pgp-encrypted',
+ _encoder=encode_7or8bit)
+
+ msg = MIMEMultipart('encrypted', micalg='pgp-sha1',
+ protocol='application/pgp-encrypted')
msg.attach(control)
msg.attach(enc)
-
- for k,v in self.headermsg.items():
- msg[k] = v
+
msg['Content-Disposition'] = 'inline'
return msg
- def signAndEncrypt(self, passphrase=None):
+ def signAndEncrypt(self, header, passphrase=None):
"""
multipart/encrypted
+-> application/pgp-encrypted (control information)
+-> application/octet-stream (body)
"""
passphrase,pass_arg = self.passphrase_arg(passphrase)
- body = self.sign()
+ body = self.sign(header, passphrase)
body.__delitem__('Bcc')
bfile = tempfile.NamedTemporaryFile()
bfile.write(flatten(body))
bfile.flush()
-
- recipient_string = ' '.join([replace(pgp_recipient_arg, 'r', recipient) for recipient in self.targetEmails()])
+
+ recipients = [replace(pgp_recipient_arg, 'r', recipient)
+ for recipient in target_emails(header)]
+ recipient_string = ' '.join(recipients)
args = replace(pgp_encrypt_only_command, 'R', recipient_string)
args = replace(args, 'f', bfile.name)
if PGP_SIGN_AS == None:
- pgp_sign_as = '<%s>' % self.sourceEmail()
+ pgp_sign_as = '<%s>' % source_email(header)
else:
pgp_sign_as = PGP_SIGN_AS
args = replace(args, 'a', pgp_sign_as)
args = replace(args, 'p', pass_arg)
status,output,error = execute(args, stdin=passphrase)
encrypted = output
-
- enc = MIMEApplication(_data=encrypted, _subtype='octet-stream', _encoder=encode_7or8bit)
+
+ enc = MIMEApplication(_data=encrypted, _subtype='octet-stream',
+ _encoder=encode_7or8bit)
enc.set_charset('us-ascii')
-
- control = MIMEApplication(_data='Version: 1\n', _subtype='pgp-encrypted', _encoder=encode_7or8bit)
-
- msg = MIMEMultipart('encrypted', micalg='pgp-sha1', protocol='application/pgp-encrypted')
+
+ control = MIMEApplication(_data='Version: 1\n',
+ _subtype='pgp-encrypted',
+ _encoder=encode_7or8bit)
+
+ msg = MIMEMultipart('encrypted', micalg='pgp-sha1',
+ protocol='application/pgp-encrypted')
msg.attach(control)
msg.attach(enc)
-
- for k,v in self.headermsg.items():
- msg[k] = v
+
msg['Content-Disposition'] = 'inline'
return msg
- def stripChanging(self, text, start, stop, replacement):
- stripping = False
- lines = []
- for line in text.splitlines():
- line.strip()
- if stripping == False:
- lines.append(line)
- if line == start:
- stripping = True
- lines.append(replacement)
- else:
- if line == stop:
- stripping = False
- lines.append(line)
- return '\n'.join(lines)
- def stripSig(self, text):
- return self.stripChanging(text,
- '-----BEGIN PGP SIGNATURE-----',
- '-----END PGP SIGNATURE-----',
- 'SIGNATURE STRIPPED (depends on current time)')
- def stripPGP(self, text):
- return self.stripChanging(text,
- '-----BEGIN PGP MESSAGE-----',
- '-----END PGP MESSAGE-----',
- 'MESSAGE STRIPPED (depends on current time)')
def test():
import doctest
if __name__ == '__main__':
from optparse import OptionParser
-
+
parser = OptionParser(usage=usage)
parser.add_option('-t', '--test', dest='test', action='store_true',
help='Run doctests and exit')
-
+
parser.add_option('-H', '--header-file', dest='header_filename',
help='file containing email header', metavar='FILE')
parser.add_option('-B', '--body-file', dest='body_filename',
help='file containing email body', metavar='FILE')
-
+
parser.add_option('-P', '--passphrase-file', dest='passphrase_file',
help='file containing gpg passphrase', metavar='FILE')
parser.add_option('-p', '--passphrase-fd', dest='passphrase_fd',
help='file descriptor from which to read gpg passphrase (0 for stdin)',
type="int", metavar='DESCRIPTOR')
-
+
parser.add_option('--mode', dest='mode', default='sign',
help="One of 'sign', 'encrypt', 'sign-encrypt', or 'plain'. Defaults to %default.",
metavar='MODE')
parser.add_option('-a', '--sign-as', dest='sign_as',
help="The gpg key to sign with (gpg's -u/--local-user)",
metavar='KEY')
-
+
parser.add_option('--output', dest='output', action='store_true',
help="Don't mail the generated message, print it to stdout instead.")
-
+
(options, args) = parser.parse_args()
-
+
stdin_used = False
-
+
if options.passphrase_file != None:
PASSPHRASE = file(options.passphrase_file, 'r').read()
elif options.passphrase_fd != None:
PASSPHRASE = sys.stdin.read()
else:
PASSPHRASE = os.read(options.passphrase_fd)
-
+
if options.sign_as:
PGP_SIGN_AS = options.sign_as
if options.test == True:
test()
sys.exit(0)
-
+
header = None
if options.header_filename != None:
if options.header_filename == '-':
- assert stdin_used == False
+ assert stdin_used == False
stdin_used = True
header = sys.stdin.read()
else:
header = file(options.header_filename, 'r').read()
if header == None:
raise Exception, "missing header"
+ headermsg = header_from_text(header)
body = None
if options.body_filename != None:
if options.body_filename == '-':
- assert stdin_used == False
+ assert stdin_used == False
stdin_used = True
body = sys.stdin.read()
else:
if body == None:
raise Exception, "missing body"
- m = Mail(header, body)
+ m = PGPMimeMessageFactory(body)
if options.mode == "sign":
- message = m.sign()
+ bodymsg = m.sign(header)
elif options.mode == "encrypt":
- message = m.encrypt()
+ bodymsg = m.encrypt(header)
elif options.mode == "sign-encrypt":
- message = m.signAndEncrypt()
+ bodymsg = m.signAndEncrypt(header)
elif options.mode == "plain":
- message = m.plain()
+ bodymsg = m.plain()
else:
print "Unrecognized mode '%s'" % options.mode
-
+
+ message = attach_root(headermsg, bodymsg)
if options.output == True:
message = flatten(message)
print message