Code

Eudora can't handle utf-8 headers. We love Eudora. (sf bug 900046)
[roundup.git] / roundup / mailer.py
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')       
47         
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.
77         
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)
107         
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):
139         
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