1 """Sending Roundup-specific mail over SMTP.
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
, specialsre
, escapesre
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):
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 def nice_sender_header(name
, address
, charset
):
29 # construct an address header so it's as human-readable as possible
30 # even in the presence of a non-ASCII name part
34 encname
= name
.encode('ASCII')
35 except UnicodeEncodeError:
36 # use Header to encode correctly.
37 encname
= Header(name
, charset
=charset
).encode()
39 # the important bits of formataddr()
40 if specialsre
.search(encname
):
41 encname
= '"%s"'%escapesre
.sub(r
'\\\g<0>', encname
)
43 # now format the header as a string - don't return a Header as anonymous
44 # headers play poorly with Messages (eg. won't get wrapped properly)
45 return '%s <%s>'%(encname
, address
)
48 """Roundup-specific mail sending."""
49 def __init__(self
, config
):
52 # set to indicate to roundup not to actually _send_ email
53 # this var must contain a file to write the mail to
54 self
.debug
= os
.environ
.get('SENDMAILDEBUG', '') \
55 or config
["MAIL_DEBUG"]
57 # set timezone so that things like formatdate(localtime=True)
58 # use the configured timezone
59 # apparently tzset doesn't exist in python under Windows, my bad.
60 # my pathetic attempts at googling a Windows-solution failed
61 # so if you're on Windows your mail won't use your configured
63 if hasattr(time
, 'tzset'):
64 os
.environ
['TZ'] = get_timezone(self
.config
.TIMEZONE
).tzname(None)
67 def get_standard_message(self
, to
, subject
, author
=None, multipart
=False):
68 '''Form a standard email message from Roundup.
70 "to" - recipients list
72 "author" - (name, address) tuple or None for admin email
74 Subject and author are encoded using the EMAIL_CHARSET from the
75 config (default UTF-8).
77 Returns a Message object.
79 # encode header values if they need to be
80 charset
= getattr(self
.config
, 'EMAIL_CHARSET', 'utf-8')
81 tracker_name
= unicode(self
.config
.TRACKER_NAME
, 'utf-8')
83 author
= (tracker_name
, self
.config
.ADMIN_EMAIL
)
86 name
= unicode(author
[0], 'utf-8')
87 author
= nice_sender_header(name
, author
[1], charset
)
90 message
= MIMEMultipart()
92 message
= MIMEText("")
93 message
.set_charset(charset
)
96 message
['Subject'] = subject
.encode('ascii')
98 message
['Subject'] = Header(subject
, charset
)
99 message
['To'] = ', '.join(to
)
100 message
['From'] = author
101 message
['Date'] = formatdate(localtime
=True)
103 # add a Precedence header so autoresponders ignore us
104 message
['Precedence'] = 'bulk'
106 # Add a unique Roundup header to help filtering
108 message
['X-Roundup-Name'] = tracker_name
.encode('ascii')
110 message
['X-Roundup-Name'] = Header(tracker_name
, charset
)
112 # and another one to avoid loops
113 message
['X-Roundup-Loop'] = 'hello'
114 # finally, an aid to debugging problems
115 message
['X-Roundup-Version'] = __version__
119 def standard_message(self
, to
, subject
, content
, author
=None):
120 """Send a standard message.
123 - to: a list of addresses usable by rfc822.parseaddr().
124 - subject: the subject as a string.
125 - content: the body of the message as a string.
126 - author: the sender as a (name, address) tuple
128 All strings are assumed to be UTF-8 encoded.
130 message
= self
.get_standard_message(to
, subject
, author
)
131 message
.set_payload(content
)
132 encode_quopri(message
)
133 self
.smtp_send(to
, message
.as_string())
135 def bounce_message(self
, bounced_message
, to
, error
,
136 subject
='Failed issue tracker submission'):
137 """Bounce a message, attaching the failed submission.
140 - bounced_message: an RFC822 Message object.
141 - to: a list of addresses usable by rfc822.parseaddr(). Might be
142 extended or overridden according to the config
143 ERROR_MESSAGES_TO setting.
144 - error: the reason of failure as a string.
145 - subject: the subject as a string.
148 # see whether we should send to the dispatcher or not
149 dispatcher_email
= getattr(self
.config
, "DISPATCHER_EMAIL",
150 getattr(self
.config
, "ADMIN_EMAIL"))
151 error_messages_to
= getattr(self
.config
, "ERROR_MESSAGES_TO", "user")
152 if error_messages_to
== "dispatcher":
153 to
= [dispatcher_email
]
154 elif error_messages_to
== "both":
155 to
.append(dispatcher_email
)
157 message
= self
.get_standard_message(to
, subject
, multipart
=True)
160 part
= MIMEText('\n'.join(error
))
163 # attach the original message to the returned message
165 for header
in bounced_message
.headers
:
168 bounced_message
.rewindbody()
169 except IOError, errmessage
:
170 body
.append("*** couldn't include message body: %s ***" %
174 body
.append(bounced_message
.fp
.read())
175 part
= MIMEText(''.join(body
))
180 self
.smtp_send(to
, message
.as_string())
181 except MessageSendError
:
182 # squash mail sending errors when bouncing mail
183 # TODO this *could* be better, as we could notify admin of the
184 # problem (even though the vast majority of bounce errors are
188 def exception_message(self
):
189 '''Send a message to the admins with information about the latest
192 subject
= '%s: %s'%(self
.config
.TRACKER_NAME
, sys
.exc_info()[1])
193 to
= [self
.config
.ADMIN_EMAIL
]
194 content
= '\n'.join(traceback
.format_exception(*sys
.exc_info()))
195 self
.standard_message(to
, subject
, content
)
197 def smtp_send(self
, to
, message
, sender
=None):
198 """Send a message over SMTP, using roundup's config.
201 - to: a list of addresses usable by rfc822.parseaddr().
202 - message: a StringIO instance with a full message.
203 - sender: if not 'None', the email address to use as the
204 envelope sender. If 'None', the admin email is used.
208 sender
= self
.config
.ADMIN_EMAIL
210 # don't send - just write to a file
211 open(self
.debug
, 'a').write('FROM: %s\nTO: %s\n%s\n' %
213 ', '.join(to
), message
))
215 # now try to send the message
217 # send the message as admin so bounces are sent there
218 # instead of to roundup
219 smtp
= SMTPConnection(self
.config
)
220 smtp
.sendmail(sender
, to
, message
)
221 except socket
.error
, value
:
222 raise MessageSendError("Error: couldn't send email: "
224 except smtplib
.SMTPException
, msg
:
225 raise MessageSendError("Error: couldn't send email: %s"%msg
)
227 class SMTPConnection(smtplib
.SMTP
):
228 ''' Open an SMTP connection to the mailhost specified in the config
230 def __init__(self
, config
):
231 smtplib
.SMTP
.__init__(self
, config
.MAILHOST
, port
=config
['MAIL_PORT'],
232 local_hostname
=config
['MAIL_LOCAL_HOSTNAME'])
234 # start the TLS if requested
235 if config
["MAIL_TLS"]:
237 self
.starttls(config
["MAIL_TLS_KEYFILE"],
238 config
["MAIL_TLS_CERTFILE"])
240 # ok, now do we also need to log in?
241 mailuser
= config
["MAIL_USERNAME"]
243 self
.login(mailuser
, config
["MAIL_PASSWORD"])
245 # vim: set et sts=4 sw=4 :