Changes in trunk/trac/notification.py [7916:9603]
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/trac/notification.py
r7916 r9603 14 14 # history and logs, available at http://trac.edgewall.org/log/. 15 15 16 import re 17 import smtplib 18 from subprocess import Popen, PIPE 16 19 import time 17 import smtplib18 import re19 20 20 21 from genshi.builder import tag 21 22 22 23 from trac import __version__ 23 from trac.config import BoolOption, IntOption, Option24 from trac.config import BoolOption, ExtensionOption, IntOption, Option 24 25 from trac.core import * 25 26 from trac.util.text import CRLF 26 from trac.util.translation import _ 27 from trac.util.translation import _, deactivate, reactivate 27 28 28 29 MAXHEADERLEN = 76 … … 35 36 ) 36 37 38 39 class IEmailSender(Interface): 40 """Extension point interface for components that allow sending e-mail.""" 41 42 def send(self, from_addr, recipients, message): 43 """Send message to recipients.""" 44 45 37 46 class NotificationSystem(Component): 38 47 48 email_sender = ExtensionOption('notification', 'email_sender', 49 IEmailSender, 'SmtpEmailSender', 50 """Name of the component implementing `IEmailSender`. 51 52 This component is used by the notification system to send emails. 53 Trac currently provides `SmtpEmailSender` for connecting to an SMTP 54 server, and `SendmailEmailSender` for running a `sendmail`-compatible 55 executable. (''since 0.12'')""") 56 39 57 smtp_enabled = BoolOption('notification', 'smtp_enabled', 'false', 40 """Enable SMTP (email) notification.""") 41 42 smtp_server = Option('notification', 'smtp_server', 'localhost', 43 """SMTP server hostname to use for email notifications.""") 44 45 smtp_port = IntOption('notification', 'smtp_port', 25, 46 """SMTP server port to use for email notification.""") 47 48 smtp_user = Option('notification', 'smtp_user', '', 49 """Username for SMTP server. (''since 0.9'').""") 50 51 smtp_password = Option('notification', 'smtp_password', '', 52 """Password for SMTP server. (''since 0.9'').""") 58 """Enable email notification.""") 53 59 54 60 smtp_from = Option('notification', 'smtp_from', 'trac@localhost', … … 70 76 71 77 smtp_default_domain = Option('notification', 'smtp_default_domain', '', 72 """Default host/domain to append to address that do not specify one""") 78 """Default host/domain to append to address that do not specify 79 one.""") 73 80 74 81 ignore_domains = Option('notification', 'ignore_domains', '', 75 82 """Comma-separated list of domains that should not be considered 76 part of email addresses (for usernames with Kerberos domains) """)83 part of email addresses (for usernames with Kerberos domains).""") 77 84 78 85 admit_domains = Option('notification', 'admit_domains', '', 79 86 """Comma-separated list of domains that should be considered as 80 valid for email addresses (such as localdomain) """)87 valid for email addresses (such as localdomain).""") 81 88 82 mime_encoding = Option('notification', 'mime_encoding', ' base64',89 mime_encoding = Option('notification', 'mime_encoding', 'none', 83 90 """Specifies the MIME encoding scheme for emails. 84 91 85 92 Valid options are 'base64' for Base64 encoding, 'qp' for 86 Quoted-Printable, and 'none' for no encoding . Note that the no encoding87 means that non-ASCII characters in text are going to cause problems88 with notifications (''since 0.10'').""")93 Quoted-Printable, and 'none' for no encoding, in which case mails will 94 be sent as 7bit if the content is all ASCII, or 8bit otherwise. 95 (''since 0.10'')""") 89 96 90 97 use_public_cc = BoolOption('notification', 'use_public_cc', 'false', 91 98 """Recipients can see email addresses of other CC'ed recipients. 92 99 93 If this option is disabled (the default), recipients are put on BCC 94 (''since 0.10'') .""")100 If this option is disabled (the default), recipients are put on BCC. 101 (''since 0.10'')""") 95 102 96 103 use_short_addr = BoolOption('notification', 'use_short_addr', 'false', 97 """Permit email address without a host/domain (i.e. username only) 104 """Permit email address without a host/domain (i.e. username only). 98 105 99 106 The SMTP server should accept those addresses, and either append 100 a FQDN or use local delivery (''since 0.10'').""") 101 102 use_tls = BoolOption('notification', 'use_tls', 'false', 103 """Use SSL/TLS to send notifications (''since 0.10'').""") 104 107 a FQDN or use local delivery. (''since 0.10'')""") 108 105 109 smtp_subject_prefix = Option('notification', 'smtp_subject_prefix', 106 110 '__default__', … … 109 113 If the setting is not defined, then the [$project_name] prefix. 110 114 If no prefix is desired, then specifying an empty option 111 will disable it.(''since 0.10.1'').""") 115 will disable it. (''since 0.10.1'').""") 116 117 def send_email(self, from_addr, recipients, message): 118 """Send message to recipients via e-mail.""" 119 self.email_sender.send(from_addr, recipients, message) 120 121 122 class SmtpEmailSender(Component): 123 """E-mail sender connecting to an SMTP server.""" 124 125 implements(IEmailSender) 126 127 smtp_server = Option('notification', 'smtp_server', 'localhost', 128 """SMTP server hostname to use for email notifications.""") 129 130 smtp_port = IntOption('notification', 'smtp_port', 25, 131 """SMTP server port to use for email notification.""") 132 133 smtp_user = Option('notification', 'smtp_user', '', 134 """Username for SMTP server. (''since 0.9'')""") 135 136 smtp_password = Option('notification', 'smtp_password', '', 137 """Password for SMTP server. (''since 0.9'')""") 138 139 use_tls = BoolOption('notification', 'use_tls', 'false', 140 """Use SSL/TLS to send notifications over SMTP. (''since 0.10'')""") 141 142 crlf = re.compile("\r?\n") 143 144 def send(self, from_addr, recipients, message): 145 # Ensure the message complies with RFC2822: use CRLF line endings 146 message = CRLF.join(self.crlf.split(message)) 147 148 self.log.info("Sending notification through SMTP at %s:%d to %s" 149 % (self.smtp_server, self.smtp_port, recipients)) 150 server = smtplib.SMTP(self.smtp_server, self.smtp_port) 151 # server.set_debuglevel(True) 152 if self.use_tls: 153 server.ehlo() 154 if not server.esmtp_features.has_key('starttls'): 155 raise TracError(_("TLS enabled but server does not support " \ 156 "TLS")) 157 server.starttls() 158 server.ehlo() 159 if self.smtp_user: 160 server.login(self.smtp_user.encode('utf-8'), 161 self.smtp_password.encode('utf-8')) 162 start = time.time() 163 server.sendmail(from_addr, recipients, message) 164 t = time.time() - start 165 if t > 5: 166 self.log.warning('Slow mail submission (%.2f s), ' 167 'check your mail setup' % t) 168 if self.use_tls: 169 # avoid false failure detection when the server closes 170 # the SMTP connection with TLS enabled 171 import socket 172 try: 173 server.quit() 174 except socket.sslerror: 175 pass 176 else: 177 server.quit() 178 179 180 class SendmailEmailSender(Component): 181 """E-mail sender using a locally-installed sendmail program.""" 182 183 implements(IEmailSender) 184 185 sendmail_path = Option('notification', 'sendmail_path', 'sendmail', 186 """Path to the sendmail executable. 187 188 The sendmail program must accept the `-i` and `-f` options. 189 (''since 0.12'')""") 190 191 def send(self, from_addr, recipients, message): 192 self.log.info("Sending notification through sendmail at %s to %s" 193 % (self.sendmail_path, recipients)) 194 cmdline = [self.sendmail_path, "-i", "-f", from_addr] 195 cmdline.extend(recipients) 196 self.log.debug("Sendmail command line: %s" % cmdline) 197 child = Popen(cmdline, bufsize=-1, stdin=PIPE, stdout=PIPE, 198 stderr=PIPE) 199 (out, err) = child.communicate(message) 200 if child.returncode or err: 201 raise Exception("Sendmail failed with (%s, %s), command: '%s'" 202 % (child.returncode, err.strip(), cmdline)) 112 203 113 204 … … 164 255 """Baseclass for notification by email.""" 165 256 166 smtp_server = 'localhost'167 smtp_port = 25168 257 from_email = 'trac+tickets@localhost' 169 258 subject = '' … … 173 262 174 263 def __init__(self, env): 175 global EMAIL_LOOKALIKE_PATTERN176 264 Notify.__init__(self, env) 177 265 … … 184 272 addrfmt = r'%s@(?:(?:%s)|%s)' % (addrfmt[:pos], addrfmt[pos+1:], 185 273 domains) 186 self.shortaddr_re = re.compile(r'%s$' % addrfmt) 187 self.longaddr_re = re.compile(r'^\s*(.*)\s+<(%s)>\s*$' % addrfmt); 188 self._use_tls = self.env.config.getbool('notification', 'use_tls') 274 self.shortaddr_re = re.compile(r'\s*(%s)\s*$' % addrfmt) 275 self.longaddr_re = re.compile(r'^\s*(.*)\s+<\s*(%s)\s*>\s*$' % addrfmt) 189 276 self._init_pref_encoding() 190 277 domains = self.env.config.get('notification', 'ignore_domains', '') … … 197 284 198 285 def _init_pref_encoding(self): 199 from email.Charset import Charset, QP, BASE64 286 from email.Charset import Charset, QP, BASE64, SHORTEST 200 287 self._charset = Charset() 201 288 self._charset.input_charset = 'utf-8' 289 self._charset.output_charset = 'utf-8' 290 self._charset.input_codec = 'utf-8' 291 self._charset.output_codec = 'utf-8' 202 292 pref = self.env.config.get('notification', 'mime_encoding').lower() 203 293 if pref == 'base64': 204 294 self._charset.header_encoding = BASE64 205 295 self._charset.body_encoding = BASE64 206 self._charset.output_charset = 'utf-8'207 self._charset.input_codec = 'utf-8'208 self._charset.output_codec = 'utf-8'209 296 elif pref in ['qp', 'quoted-printable']: 210 297 self._charset.header_encoding = QP 211 298 self._charset.body_encoding = QP 212 self._charset.output_charset = 'utf-8'213 self._charset.input_codec = 'utf-8'214 self._charset.output_codec = 'utf-8'215 299 elif pref == 'none': 216 self._charset.header_encoding = None300 self._charset.header_encoding = SHORTEST 217 301 self._charset.body_encoding = None 218 self._charset.input_codec = None219 self._charset.output_charset = 'ascii'220 302 else: 221 raise TracError(_('Invalid email encoding setting: %s' % pref)) 303 raise TracError(_('Invalid email encoding setting: %(pref)s', 304 pref=pref)) 222 305 223 306 def notify(self, resid, subject): … … 226 309 if not self.config.getbool('notification', 'smtp_enabled'): 227 310 return 228 self.smtp_server = self.config['notification'].get('smtp_server')229 self.smtp_port = self.config['notification'].getint('smtp_port')230 311 self.from_email = self.config['notification'].get('smtp_from') 231 312 self.from_name = self.config['notification'].get('smtp_from_name') … … 233 314 self.from_email = self.from_email or self.replyto_email 234 315 if not self.from_email and not self.replyto_email: 235 raise TracError(tag(tag.p('Unable to send email due to identity ' 236 'crisis.'), 237 tag.p('Neither ', tag.b('notification.from'), 238 ' nor ', tag.b('notification.reply_to'), 239 'are specified in the configuration.')), 240 'SMTP Notification Error') 241 242 # Authentication info (optional) 243 self.user_name = self.config['notification'].get('smtp_user') 244 self.password = self.config['notification'].get('smtp_password') 316 raise TracError(tag( 317 tag.p(_('Unable to send email due to identity crisis.')), 318 tag.p(_('Neither %(from_)s nor %(reply_to)s are specified ' 319 'in the configuration.', 320 from_=tag.b('notification.from'), 321 reply_to=tag.b('notification.reply_to')))), 322 _('SMTP Notification Error')) 245 323 246 324 Notify.notify(self, resid) … … 295 373 mo = self.shortaddr_re.search(address) 296 374 if mo: 297 return mo.group( 0)375 return mo.group(1) 298 376 mo = self.longaddr_re.search(address) 299 377 if mo: … … 315 393 return self.format_header(key, value) 316 394 317 def begin_send(self):318 self.server = smtplib.SMTP(self.smtp_server, self.smtp_port)319 # self.server.set_debuglevel(True)320 if self._use_tls:321 self.server.ehlo()322 if not self.server.esmtp_features.has_key('starttls'):323 raise TracError(_("TLS enabled but server does not support " \324 "TLS"))325 self.server.starttls()326 self.server.ehlo()327 if self.user_name:328 self.server.login(self.user_name.encode('utf-8'),329 self.password.encode('utf-8'))330 331 395 def send(self, torcpts, ccrcpts, mime_headers={}): 332 396 from email.MIMEText import MIMEText 333 397 from email.Utils import formatdate 334 398 stream = self.template.generate(**self.data) 335 body = stream.render('text') 336 projname = self.config.get('project', 'name') 399 # don't translate the e-mail stream 400 t = deactivate() 401 try: 402 body = stream.render('text') 403 finally: 404 reactivate(t) 405 projname = self.env.project_name 337 406 public_cc = self.config.getbool('notification', 'use_public_cc') 338 407 headers = {} … … 340 409 headers['X-Trac-Version'] = __version__ 341 410 headers['X-Trac-Project'] = projname 342 headers['X-URL'] = self. config.get('project', 'url')411 headers['X-URL'] = self.env.project_url 343 412 headers['Precedence'] = 'bulk' 344 413 headers['Auto-Submitted'] = 'auto-generated' … … 389 458 headers['Cc'] = ', '.join(pcc) 390 459 headers['Date'] = formatdate() 391 # sanity check392 if not self._charset.body_encoding:393 try:394 dummy = body.encode('ascii')395 except UnicodeDecodeError:396 raise TracError(_("Ticket contains non-ASCII chars. " \397 "Please change encoding setting"))398 460 msg = MIMEText(body, 'plain') 399 461 # Message class computes the wrong type from MIMEText constructor, … … 402 464 del msg['Content-Transfer-Encoding'] 403 465 msg.set_charset(self._charset) 404 self.add_headers(msg, headers); 405 self.add_headers(msg, mime_headers); 406 self.env.log.info("Sending SMTP notification to %s:%d to %s" 407 % (self.smtp_server, self.smtp_port, recipients)) 408 msgtext = msg.as_string() 409 # Ensure the message complies with RFC2822: use CRLF line endings 410 recrlf = re.compile("\r?\n") 411 msgtext = CRLF.join(recrlf.split(msgtext)) 412 start = time.time() 413 self.server.sendmail(msg['From'], recipients, msgtext) 414 t = time.time() - start 415 if t > 5: 416 self.env.log.warning('Slow mail submission (%.2f s), ' 417 'check your mail setup' % t) 418 419 def finish_send(self): 420 if self._use_tls: 421 # avoid false failure detection when the server closes 422 # the SMTP connection with TLS enabled 423 import socket 424 try: 425 self.server.quit() 426 except socket.sslerror: 427 pass 428 else: 429 self.server.quit() 466 self.add_headers(msg, headers) 467 self.add_headers(msg, mime_headers) 468 NotificationSystem(self.env).send_email(self.from_email, recipients, 469 msg.as_string())
Note:
See TracChangeset
for help on using the changeset viewer.