Skip to content

Commit

Permalink
Allow emailing with non-ascii attachments.
Browse files Browse the repository at this point in the history
  • Loading branch information
keith.dart committed Apr 25, 2014
1 parent 9c55e7c commit 50ca18a
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 57 deletions.
12 changes: 7 additions & 5 deletions QA/pycopia/reports/Email.py
@@ -1,6 +1,6 @@
#!/usr/bin/python2.4
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
#
#
# $Id$
#
# Copyright (C) 1999-2006 Keith Dart <keith@kdart.com>
Expand All @@ -23,13 +23,15 @@
import os
from cStringIO import StringIO

import chardet

from pycopia import reports
NO_MESSAGE = reports.NO_MESSAGE

from pycopia import ezmail

class EmailReport(reports.NullReport):
"""Create an a report that is emailed, rather than written to a file.
"""Create an a report that is emailed, rather than written to a file.
EmailReport(
[formatter="text/plain"], # formatter type
[recipients=None], # list of recipients, or None. If none the
Expand Down Expand Up @@ -67,17 +69,17 @@ def writeline(self, text):
def finalize(self):
"""finalizing this Report sends off the email."""
self.write(self._formatter.finalize())
report = ezmail.MIMEText.MIMEText(self._fo.getvalue(),
report = ezmail.MIMEText.MIMEText(self._fo.getvalue(),
self._formatter.MIMETYPE.split("/")[1])
report["Content-Disposition"] = "inline"
self._message.attach(report)
if self._attach_logfile and self._logfile:
try:
lfd = open(self._logfile).read()
lfd = open(self._logfile, "rb").read()
except:
pass # non-fatal
else:
logmsg = ezmail.MIMEText.MIMEText(lfd)
logmsg = ezmail.MIMEText.MIMEText(lfd, charset=chardet.detect(lfd))
logmsg["Content-Disposition"] = 'attachment; filename=%s' % (
os.path.basename(self._logfile), )
self._message.attach(logmsg)
Expand Down
37 changes: 16 additions & 21 deletions core/pycopia/ezmail.py
@@ -1,8 +1,6 @@
#!/usr/bin/python2.4
#!/usr/bin/python2.7
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
#
# $Id$
#
# Copyright (C) 1999-2006 Keith Dart <keith@kdart.com>
#
# This library is free software; you can redistribute it and/or
Expand Down Expand Up @@ -59,7 +57,7 @@ def formatdate(timeval=None):


class SimpleMessage(Message.Message):
def __init__(self, _text, mimetype="text/plain", charset="us-ascii"):
def __init__(self, _text, mimetype="text/plain", charset="utf-8"):
Message.Message.__init__(self)
self['MIME-Version'] = '1.0'
self.add_header('Content-Type', mimetype)
Expand Down Expand Up @@ -146,8 +144,7 @@ def _add_recipient(self, header, addr, name):


class AutoMessage(SimpleMessage, AutoMessageMixin):
def __init__(self, text, mimetype="text/plain", charset="us-ascii",
From=None, To=None):
def __init__(self, text, mimetype="text/plain", charset="utf-8", From=None, To=None):
AutoMessageMixin.__init__(self, From, To)
SimpleMessage.__init__(self, text, mimetype, charset)

Expand Down Expand Up @@ -193,14 +190,14 @@ def message_from_string(s, klass=SimpleMessage, strict=0):
parser = get_parser(klass, strict)
return parser.parsestr(s)

def self_address():
def self_address(domain=None):
"""self_address()
Returns address string referring to user running this (yourself)."""
global CONFIG
name, longname = getuser()
domain = CONFIG.get("domain")
if domain:
return "%s@%s" % (name, domain), longname
dom = domain or CONFIG.get("domain")
if dom:
return "%s@%s" % (name, dom), longname
else:
return "%s@%s" % (name, _get_hostname()), longname

Expand Down Expand Up @@ -235,8 +232,7 @@ def _do_attach(multipart, obj):
multipart.attach(msg)


def ezmail(obj, To=None, From=None, subject=None, cc=None, bcc=None,
extra_headers=None):
def ezmail(obj, To=None, From=None, subject=None, cc=None, bcc=None, extra_headers=None, mailhost=None):
"""A generic mailer that sends a multipart-mixed message with attachments.
The 'obj' parameter may be a MIME* message, or another type of object that
will be converted to text. If it is a list, each element of the list will
Expand All @@ -251,8 +247,10 @@ def ezmail(obj, To=None, From=None, subject=None, cc=None, bcc=None,
outer = MultipartMessage()
for part in obj:
_do_attach(outer, part)
elif isinstance(obj, unicode):
outer = AutoMessage(obj, charset="utf-8")
else:
outer = AutoMessage(str(obj).encode("us-ascii"))
outer = AutoMessage(unicode(obj), charset="utf-8")

outer.From(From)
if To:
Expand All @@ -268,16 +266,16 @@ def ezmail(obj, To=None, From=None, subject=None, cc=None, bcc=None,
for name, value in extra_headers.items():
outer[name] = value

mailhost = CONFIG.get("mailhost", "localhost")
mhost = mailhost or CONFIG.get("mailhost", "localhost")

if mailhost == "localhost":
if mhost == "localhost":
smtp = LocalSender()
status = outer.send(smtp)
if not status:
raise MailError(str(status))
else:
from pycopia.inet import SMTP
smtp = SMTP.SMTP(mailhost, bindto=CONFIG.get("bindto"))
smtp = SMTP.SMTP(mhost, bindto=CONFIG.get("bindto"))
errs = outer.send(smtp)
smtp.quit()
if errs:
Expand All @@ -286,10 +284,9 @@ def ezmail(obj, To=None, From=None, subject=None, cc=None, bcc=None,
return outer["Message-ID"]


def mail(obj, To=None, From=None, subject=None, cc=None, bcc=None,
extra_headers=None):
def mail(obj, To=None, From=None, subject=None, cc=None, bcc=None, extra_headers=None, mailhost=None):
try:
return ezmail(obj, To, From, subject, cc, bcc, extra_headers)
return ezmail(obj, To, From, subject, cc, bcc, extra_headers, mailhost)
except MailError as err:
print("Error while sending mail!", file=sys.stderr)
print(err, file=sys.stderr)
Expand All @@ -316,8 +313,6 @@ def sendmail(self, From, rcpt_to, msg, mopts=None, rcptopts=None):
proc.wait()
return proc.exitstatus

def dp(proc):
print(proc)

# global configuration
CONFIG = get_config()
Expand Down
61 changes: 30 additions & 31 deletions core/pycopia/inet/SMTP.py
Expand Up @@ -41,7 +41,7 @@
sendmail-bugs@sendmail.org.
For local information send email to Postmaster at your site.
End of HELP info
>>> s.putcmd("vrfy","someone@here")
>>> s.putcmd(b"vrfy","someone@here")
>>> s.getreply()
(250, "Somebody OverHere <somebody@here.my.org>")
>>> s.quit()
Expand Down Expand Up @@ -71,7 +71,8 @@


SMTP_PORT = 25
CRLF="\r\n"
CRLF=b"\r\n"
DOTCRLF=b".\r\n"

OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I)

Expand Down Expand Up @@ -324,7 +325,7 @@ def _connect(self, addr, retries):
def send(self, s):
"""Send string to the server."""
if self.logfile:
self.logfile.write('send: %r\n' % (s,))
self.logfile.write(b'send: %r\n' % (s,))
if self.sock:
try:
self.sock.sendall(s)
Expand All @@ -337,9 +338,9 @@ def send(self, s):
def putcmd(self, cmd, args=""):
"""Send a command to the server."""
if args == "":
out = '%s%s' % (cmd, CRLF)
out = b'%s%s' % (cmd, CRLF)
else:
out = '%s %s%s' % (cmd, args, CRLF)
out = b'%s %s%s' % (cmd, args, CRLF)
self.send(out.encode("ascii"))

def getreply(self):
Expand Down Expand Up @@ -390,7 +391,7 @@ def getreply(self):

def docmd(self, cmd, args=""):
"""Send a command, and return its response code."""
self.putcmd(cmd,args)
self.putcmd(cmd, args)
return self.getreply()

# std smtp commands
Expand All @@ -401,9 +402,9 @@ def helo(self, name=''):
"""
name = name or self._bindto
if name:
self.putcmd("helo", name)
self.putcmd(b"helo", name)
else:
self.putcmd("helo", socket.getfqdn())
self.putcmd(b"helo", socket.getfqdn())
(code,msg)=self.getreply()
self.helo_resp=msg
return (code,msg)
Expand All @@ -416,9 +417,9 @@ def ehlo(self, name=''):
self.esmtp_features = {}
name = name or self._bindto
if name:
self.putcmd("ehlo", name)
self.putcmd(b"ehlo", name)
else:
self.putcmd("ehlo", socket.getfqdn())
self.putcmd(b"ehlo", socket.getfqdn())
(code,msg)=self.getreply()
# According to RFC1869 some (badly written)
# MTA's will disconnect on an ehlo. Toss an exception if
Expand Down Expand Up @@ -469,7 +470,7 @@ def has_extn(self, opt):
def help(self, args=''):
"""SMTP 'help' command.
Returns help text from server."""
self.putcmd("help", args)
self.putcmd(b"help", args)
return self.getreply()

def rset(self):
Expand All @@ -480,20 +481,20 @@ def noop(self):
"""SMTP 'noop' command -- doesn't do anything :>"""
return self.docmd("noop")

def mail(self,sender, options=[]):
def mail(self,sender, options=None):
"""SMTP 'mail' command -- begins mail xfer session."""
optionlist = ''
if options and self.does_esmtp:
optionlist = ' ' + ' '.join(options)
self.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender) ,optionlist))
self.putcmd(b"mail", b"FROM:%s%s" % (quoteaddr(sender) ,optionlist))
return self.getreply()

def rcpt(self,recip,options=[]):
def rcpt(self,recip, options=None):
"""SMTP 'rcpt' command -- indicates 1 recipient for this mail."""
optionlist = ''
if options and self.does_esmtp:
optionlist = ' ' + ' '.join(options)
self.putcmd("rcpt","TO:%s%s" % (quoteaddr(recip),optionlist))
self.putcmd(b"rcpt", b"TO:%s%s" % (quoteaddr(recip),optionlist))
return self.getreply()

def data(self,msg):
Expand All @@ -504,7 +505,7 @@ def data(self,msg):
DATA command; the return value from this method is the final
response code received when the all data is sent.
"""
self.putcmd("data")
self.putcmd(b"data")
(code,repl)=self.getreply()
if self.logfile:
self.logfile.write("data: %s %s\n" % (code,repl))
Expand All @@ -513,8 +514,8 @@ def data(self,msg):
else:
q = quotedata(msg)
if q[-2:] != CRLF:
q = q + CRLF
q = q + "." + CRLF
q += CRLF
q += DOTCRLF
self.send(q)
(code, msg)=self.getreply()
if self.logfile:
Expand All @@ -523,14 +524,14 @@ def data(self,msg):

def verify(self, address):
"""SMTP 'verify' command -- checks for address validity."""
self.putcmd("vrfy", quoteaddr(address))
self.putcmd(b"vrfy", quoteaddr(address))
return self.getreply()
# a.k.a.
vrfy=verify

def expn(self, address):
"""SMTP 'verify' command -- checks for address validity."""
self.putcmd("expn", quoteaddr(address))
self.putcmd(b"expn", quoteaddr(address))
return self.getreply()

# some useful methods
Expand Down Expand Up @@ -566,9 +567,9 @@ def encode_plain(user, password):
return encode_base64("%s\0%s\0%s" % (user, user, password), eol="")


AUTH_PLAIN = "PLAIN"
AUTH_CRAM_MD5 = "CRAM-MD5"
AUTH_LOGIN = "LOGIN"
AUTH_PLAIN = b"PLAIN"
AUTH_CRAM_MD5 = b"CRAM-MD5"
AUTH_LOGIN = b"LOGIN"

if self.helo_resp is None and self.ehlo_resp is None:
if not (200 <= self.ehlo()[0] <= 299):
Expand Down Expand Up @@ -633,8 +634,7 @@ def starttls(self, keyfile = None, certfile = None):
self.file = SSLFakeFile(sslobj)
return (resp, reply)

def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
rcpt_options=[]):
def sendmail(self, from_addr, to_addrs, msg, mail_options=None, rcpt_options=None):
"""This command performs an entire mail transaction.
The arguments are::
Expand Down Expand Up @@ -691,7 +691,6 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
empty dictionary.
"""
msg = msg.encode("ascii")
if self.helo_resp is None and self.ehlo_resp is None:
if not (200 <= self.ehlo()[0] <= 299):
(code,resp) = self.helo()
Expand All @@ -701,8 +700,9 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
if self.does_esmtp:
if self.has_extn('size'):
esmtp_opts.append("size={0:d}".format(len(msg)))
for option in mail_options:
esmtp_opts.append(option)
if mail_options:
for option in mail_options:
esmtp_opts.append(option)

(code,resp) = self.mail(from_addr, esmtp_opts)
if code != 250:
Expand Down Expand Up @@ -815,9 +815,8 @@ def parse_data(self, parser):
if self.message:
self.data = None

def send(self, smtp, mail_options=[], rcpt_options=[]):
"""send(smtp_client, mail_options=[], rcpt_options=[])
Mails this envelope using the supplied SMTP client object."""
def send(self, smtp, mail_options=None, rcpt_options=None):
"""Mails this envelope using the supplied SMTP client object."""
if self.message:
return smtp.sendmail(self.mail_from, self.rcpt_to, self.message.as_string(), mail_options, rcpt_options)
elif self.data:
Expand Down

0 comments on commit 50ca18a

Please sign in to comment.