1 """Sending Roundup-specific mail over SMTP.
2 """
3 __docformat__ = 'restructuredtext'
4 # $Id: mailer.py,v 1.6 2004-02-23 05:29:05 richard Exp $
6 import time, quopri, os, socket, smtplib, re
8 from cStringIO import StringIO
9 from MimeWriter import MimeWriter
11 from roundup.rfc2822 import encode_header
12 from roundup import __version__
14 class MessageSendError(RuntimeError):
15 pass
17 class Mailer:
18 """Roundup-specific mail sending."""
19 def __init__(self, config):
20 self.config = config
22 # set to indicate to roundup not to actually _send_ email
23 # this var must contain a file to write the mail to
24 self.debug = os.environ.get('SENDMAILDEBUG', '')
26 def get_standard_message(self, to, subject, author=None):
27 if not author:
28 author = straddr((self.config.TRACKER_NAME,
29 self.config.ADMIN_EMAIL))
30 message = StringIO()
31 writer = MimeWriter(message)
32 writer.addheader('Subject', encode_header(subject,
33 self.config.EMAIL_CHARSET))
34 writer.addheader('To', ', '.join(to))
35 writer.addheader('From', author)
36 writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000",
37 time.gmtime()))
39 # Add a unique Roundup header to help filtering
40 writer.addheader('X-Roundup-Name', self.config.TRACKER_NAME)
41 # and another one to avoid loops
42 writer.addheader('X-Roundup-Loop', 'hello')
43 # finally, an aid to debugging problems
44 writer.addheader('X-Roundup-Version', __version__)
46 writer.addheader('MIME-Version', '1.0')
48 return message, writer
50 def standard_message(self, to, subject, content, author=None):
51 """Send a standard message.
53 Arguments:
54 - to: a list of addresses usable by rfc822.parseaddr().
55 - subject: the subject as a string.
56 - content: the body of the message as a string.
57 - author: the sender as a string, suitable for a 'From:' header.
58 """
59 message, writer = self.get_standard_message(to, subject, author)
61 writer.addheader('Content-Transfer-Encoding', 'quoted-printable')
62 body = writer.startbody('text/plain; charset=utf-8')
63 content = StringIO(content)
64 quopri.encode(content, body, 0)
66 self.smtp_send(to, message)
68 def bounce_message(self, bounced_message, to, error,
69 subject='Failed issue tracker submission'):
70 """Bounce a message, attaching the failed submission.
72 Arguments:
73 - bounced_message: an RFC822 Message object.
74 - to: a list of addresses usable by rfc822.parseaddr().
75 - error: the reason of failure as a string.
76 - subject: the subject as a string.
78 """
79 message, writer = self.get_standard_message(to, subject)
81 part = writer.startmultipartbody('mixed')
82 part = writer.nextpart()
83 part.addheader('Content-Transfer-Encoding', 'quoted-printable')
84 body = part.startbody('text/plain; charset=utf-8')
85 body.write('\n'.join(error))
87 # attach the original message to the returned message
88 part = writer.nextpart()
89 part.addheader('Content-Disposition', 'attachment')
90 part.addheader('Content-Description', 'Message you sent')
91 body = part.startbody('text/plain')
93 for header in bounced_message.headers:
94 body.write(header)
95 body.write('\n')
96 try:
97 bounced_message.rewindbody()
98 except IOError, message:
99 body.write("*** couldn't include message body: %s ***"
100 % bounced_message)
101 else:
102 body.write(bounced_message.fp.read())
104 writer.lastpart()
106 self.smtp_send(to, message)
108 def smtp_send(self, to, message):
109 """Send a message over SMTP, using roundup's config.
111 Arguments:
112 - to: a list of addresses usable by rfc822.parseaddr().
113 - message: a StringIO instance with a full message.
114 """
115 if self.debug:
116 # don't send - just write to a file
117 open(self.debug, 'a').write('FROM: %s\nTO: %s\n%s\n' %
118 (self.config.ADMIN_EMAIL,
119 ', '.join(to),
120 message.getvalue()))
121 else:
122 # now try to send the message
123 try:
124 # send the message as admin so bounces are sent there
125 # instead of to roundup
126 smtp = SMTPConnection(self.config)
127 smtp.sendmail(self.config.ADMIN_EMAIL, to,
128 message.getvalue())
129 except socket.error, value:
130 raise MessageSendError("Error: couldn't send email: "
131 "mailhost %s"%value)
132 except smtplib.SMTPException, msg:
133 raise MessageSendError("Error: couldn't send email: %s"%msg)
135 class SMTPConnection(smtplib.SMTP):
136 ''' Open an SMTP connection to the mailhost specified in the config
137 '''
138 def __init__(self, config):
140 smtplib.SMTP.__init__(self, config.MAILHOST)
142 # use TLS?
143 use_tls = getattr(config, 'MAILHOST_TLS', 'no')
144 if use_tls == 'yes':
145 # do we have key files too?
146 keyfile = getattr(config, 'MAILHOST_TLS_KEYFILE', '')
147 if keyfile:
148 certfile = getattr(config, 'MAILHOST_TLS_CERTFILE', '')
149 if certfile:
150 args = (keyfile, certfile)
151 else:
152 args = (keyfile, )
153 else:
154 args = ()
155 # start the TLS
156 self.starttls(*args)
158 # ok, now do we also need to log in?
159 mailuser = getattr(config, 'MAILUSER', None)
160 if mailuser:
161 self.login(*config.MAILUSER)
163 # use the 'email' module, either imported, or our copied version
164 try :
165 from email.Utils import formataddr as straddr
166 except ImportError :
167 # code taken from the email package 2.4.3
168 def straddr(pair, specialsre = re.compile(r'[][\()<>@,:;".]'),
169 escapesre = re.compile(r'[][\()"]')):
170 name, address = pair
171 if name:
172 quotes = ''
173 if specialsre.search(name):
174 quotes = '"'
175 name = escapesre.sub(r'\\\g<0>', name)
176 return '%s%s%s <%s>' % (quotes, name, quotes, address)
177 return address