171ec1133c8ebc12fc5a7a8bb5f99da64433241d
1 """Sending Roundup-specific mail over SMTP.
2 """
3 __docformat__ = 'restructuredtext'
5 import time, quopri, os, socket, smtplib, re, sys, traceback, email
7 from cStringIO import StringIO
9 from roundup import __version__
10 from roundup.date import get_timezone
12 from email.Utils import formatdate, formataddr
13 from email.Message import Message
14 from email.Header import Header
15 from email.MIMEText import MIMEText
16 from email.MIMEMultipart import MIMEMultipart
18 class MessageSendError(RuntimeError):
19 pass
21 def encode_quopri(msg):
22 orig = msg.get_payload()
23 encdata = quopri.encodestring(orig)
24 msg.set_payload(encdata)
25 del msg['Content-Transfer-Encoding']
26 msg['Content-Transfer-Encoding'] = 'quoted-printable'
28 class Mailer:
29 """Roundup-specific mail sending."""
30 def __init__(self, config):
31 self.config = config
33 # set to indicate to roundup not to actually _send_ email
34 # this var must contain a file to write the mail to
35 self.debug = os.environ.get('SENDMAILDEBUG', '') \
36 or config["MAIL_DEBUG"]
38 # set timezone so that things like formatdate(localtime=True)
39 # use the configured timezone
40 # apparently tzset doesn't exist in python under Windows, my bad.
41 # my pathetic attempts at googling a Windows-solution failed
42 # so if you're on Windows your mail won't use your configured
43 # timezone.
44 if hasattr(time, 'tzset'):
45 os.environ['TZ'] = get_timezone(self.config.TIMEZONE).tzname(None)
46 time.tzset()
48 def get_standard_message(self, to, subject, author=None, multipart=False):
49 '''Form a standard email message from Roundup.
51 "to" - recipients list
52 "subject" - Subject
53 "author" - (name, address) tuple or None for admin email
55 Subject and author are encoded using the EMAIL_CHARSET from the
56 config (default UTF-8).
58 Returns a Message object.
59 '''
60 # encode header values if they need to be
61 charset = getattr(self.config, 'EMAIL_CHARSET', 'utf-8')
62 tracker_name = unicode(self.config.TRACKER_NAME, 'utf-8')
63 if not author:
64 author = formataddr((tracker_name, self.config.ADMIN_EMAIL))
65 else:
66 name = unicode(author[0], 'utf-8')
67 author = formataddr((name, author[1]))
69 if multipart:
70 message = MIMEMultipart()
71 else:
72 message = MIMEText("")
73 message.set_charset(charset)
75 try:
76 message['Subject'] = subject.encode('ascii')
77 except UnicodeError:
78 message['Subject'] = Header(subject, charset)
79 message['To'] = ', '.join(to)
80 try:
81 message['From'] = author.encode('ascii')
82 except UnicodeError:
83 message['From'] = Header(author, charset)
84 message['Date'] = formatdate(localtime=True)
86 # add a Precedence header so autoresponders ignore us
87 message['Precedence'] = 'bulk'
89 # Add a unique Roundup header to help filtering
90 try:
91 message['X-Roundup-Name'] = tracker_name.encode('ascii')
92 except UnicodeError:
93 message['X-Roundup-Name'] = Header(tracker_name, charset)
95 # and another one to avoid loops
96 message['X-Roundup-Loop'] = 'hello'
97 # finally, an aid to debugging problems
98 message['X-Roundup-Version'] = __version__
100 return message
102 def standard_message(self, to, subject, content, author=None):
103 """Send a standard message.
105 Arguments:
106 - to: a list of addresses usable by rfc822.parseaddr().
107 - subject: the subject as a string.
108 - content: the body of the message as a string.
109 - author: the sender as a (name, address) tuple
111 All strings are assumed to be UTF-8 encoded.
112 """
113 message = self.get_standard_message(to, subject, author)
114 message.set_payload(content)
115 encode_quopri(message)
116 self.smtp_send(to, message.as_string())
118 def bounce_message(self, bounced_message, to, error,
119 subject='Failed issue tracker submission'):
120 """Bounce a message, attaching the failed submission.
122 Arguments:
123 - bounced_message: an RFC822 Message object.
124 - to: a list of addresses usable by rfc822.parseaddr(). Might be
125 extended or overridden according to the config
126 ERROR_MESSAGES_TO setting.
127 - error: the reason of failure as a string.
128 - subject: the subject as a string.
130 """
131 # see whether we should send to the dispatcher or not
132 dispatcher_email = getattr(self.config, "DISPATCHER_EMAIL",
133 getattr(self.config, "ADMIN_EMAIL"))
134 error_messages_to = getattr(self.config, "ERROR_MESSAGES_TO", "user")
135 if error_messages_to == "dispatcher":
136 to = [dispatcher_email]
137 elif error_messages_to == "both":
138 to.append(dispatcher_email)
140 message = self.get_standard_message(to, subject, multipart=True)
142 # add the error text
143 part = MIMEText('\n'.join(error))
144 message.attach(part)
146 # attach the original message to the returned message
147 body = []
148 for header in bounced_message.headers:
149 body.append(header)
150 try:
151 bounced_message.rewindbody()
152 except IOError, errmessage:
153 body.append("*** couldn't include message body: %s ***" %
154 errmessage)
155 else:
156 body.append('\n')
157 body.append(bounced_message.fp.read())
158 part = MIMEText(''.join(body))
159 message.attach(part)
161 # send
162 try:
163 self.smtp_send(to, message.as_string())
164 except MessageSendError:
165 # squash mail sending errors when bouncing mail
166 # TODO this *could* be better, as we could notify admin of the
167 # problem (even though the vast majority of bounce errors are
168 # because of spam)
169 pass
171 def exception_message(self):
172 '''Send a message to the admins with information about the latest
173 traceback.
174 '''
175 subject = '%s: %s'%(self.config.TRACKER_NAME, sys.exc_info()[1])
176 to = [self.config.ADMIN_EMAIL]
177 content = '\n'.join(traceback.format_exception(*sys.exc_info()))
178 self.standard_message(to, subject, content)
180 def smtp_send(self, to, message):
181 """Send a message over SMTP, using roundup's config.
183 Arguments:
184 - to: a list of addresses usable by rfc822.parseaddr().
185 - message: a StringIO instance with a full message.
186 """
187 if self.debug:
188 # don't send - just write to a file
189 open(self.debug, 'a').write('FROM: %s\nTO: %s\n%s\n' %
190 (self.config.ADMIN_EMAIL,
191 ', '.join(to), message))
192 else:
193 # now try to send the message
194 try:
195 # send the message as admin so bounces are sent there
196 # instead of to roundup
197 smtp = SMTPConnection(self.config)
198 smtp.sendmail(self.config.ADMIN_EMAIL, to, message)
199 except socket.error, value:
200 raise MessageSendError("Error: couldn't send email: "
201 "mailhost %s"%value)
202 except smtplib.SMTPException, msg:
203 raise MessageSendError("Error: couldn't send email: %s"%msg)
205 class SMTPConnection(smtplib.SMTP):
206 ''' Open an SMTP connection to the mailhost specified in the config
207 '''
208 def __init__(self, config):
209 smtplib.SMTP.__init__(self, config.MAILHOST, port=config['MAIL_PORT'],
210 local_hostname=config['MAIL_LOCAL_HOSTNAME'])
212 # start the TLS if requested
213 if config["MAIL_TLS"]:
214 self.ehlo()
215 self.starttls(config["MAIL_TLS_KEYFILE"],
216 config["MAIL_TLS_CERTFILE"])
218 # ok, now do we also need to log in?
219 mailuser = config["MAIL_USERNAME"]
220 if mailuser:
221 self.login(mailuser, config["MAIL_PASSWORD"])
223 # vim: set et sts=4 sw=4 :