Premiere version : mise en route du suivi.
[auf_roundup.git] / roundup / .svn / text-base / mailer.py.svn-base
1 """Sending Roundup-specific mail over SMTP.
2 """
3 __docformat__ = 'restructuredtext'
4
5 import time, quopri, os, socket, smtplib, re, sys, traceback, email
6
7 from cStringIO import StringIO
8
9 from roundup import __version__
10 from roundup.date import get_timezone
11
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
17
18 class MessageSendError(RuntimeError):
19     pass
20
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'
27
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
31     if not name:
32         return address
33     try:
34         encname = name.encode('ASCII')
35     except UnicodeEncodeError:
36         # use Header to encode correctly.
37         encname = Header(name, charset=charset).encode()
38
39     # the important bits of formataddr()
40     if specialsre.search(encname):
41         encname = '"%s"'%escapesre.sub(r'\\\g<0>', encname)
42
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)
46
47 class Mailer:
48     """Roundup-specific mail sending."""
49     def __init__(self, config):
50         self.config = config
51
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"]
56
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
62         # timezone.
63         if hasattr(time, 'tzset'):
64             os.environ['TZ'] = get_timezone(self.config.TIMEZONE).tzname(None)
65             time.tzset()
66
67     def get_standard_message(self, to, subject, author=None, multipart=False):
68         '''Form a standard email message from Roundup.
69
70         "to"      - recipients list
71         "subject" - Subject
72         "author"  - (name, address) tuple or None for admin email
73
74         Subject and author are encoded using the EMAIL_CHARSET from the
75         config (default UTF-8).
76
77         Returns a Message object.
78         '''
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')
82         if not author:
83             author = (tracker_name, self.config.ADMIN_EMAIL)
84             name = author[0]
85         else:
86             name = unicode(author[0], 'utf-8')
87         author = nice_sender_header(name, author[1], charset)
88
89         if multipart:
90             message = MIMEMultipart()
91         else:
92             message = MIMEText("")
93             message.set_charset(charset)
94
95         try:
96             message['Subject'] = subject.encode('ascii')
97         except UnicodeError:
98             message['Subject'] = Header(subject, charset)
99         message['To'] = ', '.join(to)
100         message['From'] = author
101         message['Date'] = formatdate(localtime=True)
102
103         # add a Precedence header so autoresponders ignore us
104         message['Precedence'] = 'bulk'
105
106         # Add a unique Roundup header to help filtering
107         try:
108             message['X-Roundup-Name'] = tracker_name.encode('ascii')
109         except UnicodeError:
110             message['X-Roundup-Name'] = Header(tracker_name, charset)
111
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__
116
117         return message
118
119     def standard_message(self, to, subject, content, author=None):
120         """Send a standard message.
121
122         Arguments:
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
127
128         All strings are assumed to be UTF-8 encoded.
129         """
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())
134
135     def bounce_message(self, bounced_message, to, error,
136                        subject='Failed issue tracker submission'):
137         """Bounce a message, attaching the failed submission.
138
139         Arguments:
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.
146
147         """
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)
156
157         message = self.get_standard_message(to, subject, multipart=True)
158
159         # add the error text
160         part = MIMEText('\n'.join(error))
161         message.attach(part)
162
163         # attach the original message to the returned message
164         body = []
165         for header in bounced_message.headers:
166             body.append(header)
167         try:
168             bounced_message.rewindbody()
169         except IOError, errmessage:
170             body.append("*** couldn't include message body: %s ***" %
171                 errmessage)
172         else:
173             body.append('\n')
174             body.append(bounced_message.fp.read())
175         part = MIMEText(''.join(body))
176         message.attach(part)
177
178         # send
179         try:
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
185             # because of spam)
186             pass
187
188     def exception_message(self):
189         '''Send a message to the admins with information about the latest
190         traceback.
191         '''
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)
196
197     def smtp_send(self, to, message, sender=None):
198         """Send a message over SMTP, using roundup's config.
199
200         Arguments:
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.
205         """
206
207         if not sender:
208             sender = self.config.ADMIN_EMAIL
209         if self.debug:
210             # don't send - just write to a file
211             open(self.debug, 'a').write('FROM: %s\nTO: %s\n%s\n' %
212                                         (sender,
213                                          ', '.join(to), message))
214         else:
215             # now try to send the message
216             try:
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: "
223                                        "mailhost %s"%value)
224             except smtplib.SMTPException, msg:
225                 raise MessageSendError("Error: couldn't send email: %s"%msg)
226
227 class SMTPConnection(smtplib.SMTP):
228     ''' Open an SMTP connection to the mailhost specified in the config
229     '''
230     def __init__(self, config):
231         smtplib.SMTP.__init__(self, config.MAILHOST, port=config['MAIL_PORT'],
232                               local_hostname=config['MAIL_LOCAL_HOSTNAME'])
233
234         # start the TLS if requested
235         if config["MAIL_TLS"]:
236             self.ehlo()
237             self.starttls(config["MAIL_TLS_KEYFILE"],
238                 config["MAIL_TLS_CERTFILE"])
239
240         # ok, now do we also need to log in?
241         mailuser = config["MAIL_USERNAME"]
242         if mailuser:
243             self.login(mailuser, config["MAIL_PASSWORD"])
244
245 # vim: set et sts=4 sw=4 :