Package trac :: Package notification :: Module mail

Source Code for Module trac.notification.mail

  1  # -*- coding: utf-8 -*- 
  2  # 
  3  # Copyright (C) 2003-2020 Edgewall Software 
  4  # Copyright (C) 2003-2005 Daniel Lundin <[email protected]> 
  5  # Copyright (C) 2005-2006 Emmanuel Blot <[email protected]> 
  6  # Copyright (C) 2008 Stephen Hansen 
  7  # Copyright (C) 2009 Robert Corsaro 
  8  # Copyright (C) 2010-2012 Steffen Hoffmann 
  9  # All rights reserved. 
 10  # 
 11  # This software is licensed as described in the file COPYING, which 
 12  # you should have received as part of this distribution. The terms 
 13  # are also available at https://trac.edgewall.org/wiki/TracLicense. 
 14  # 
 15  # This software consists of voluntary contributions made by many 
 16  # individuals. For the exact contribution history, see the revision 
 17  # history and logs, available at https://trac.edgewall.org/log/. 
 18   
 19  import hashlib 
 20  import os 
 21  import re 
 22  import smtplib 
 23  from email.charset import BASE64, QP, SHORTEST, Charset 
 24  from email.header import Header 
 25  from email.mime.multipart import MIMEMultipart 
 26  from email.mime.text import MIMEText 
 27  from email.utils import formatdate, parseaddr, getaddresses 
 28  from subprocess import Popen, PIPE 
 29   
 30  from genshi.builder import tag 
 31   
 32  from trac.config import (BoolOption, ConfigurationError, IntOption, Option, 
 33                           OrderedExtensionsOption) 
 34  from trac.core import Component, ExtensionPoint, TracError, implements 
 35  from trac.notification.api import ( 
 36      get_target_id, IEmailAddressResolver, IEmailDecorator, IEmailSender, 
 37      INotificationDistributor, INotificationFormatter, INotificationSubscriber, 
 38      NotificationSystem) 
 39  from trac.util import lazy 
 40  from trac.util.compat import close_fds 
 41  from trac.util.datefmt import time_now, to_utimestamp 
 42  from trac.util.text import CRLF, exception_to_unicode, fix_eol, to_unicode 
 43  from trac.util.translation import _, tag_ 
 44  from trac.web.session import get_session_attribute 
 45   
 46   
 47  __all__ = ['AlwaysEmailSubscriber', 'EMAIL_LOOKALIKE_PATTERN', 
 48             'EmailDistributor', 'FromAuthorEmailDecorator', 'MAXHEADERLEN', 
 49             'RecipientMatcher', 'SendmailEmailSender', 'SessionEmailResolver', 
 50             'SmtpEmailSender', 'create_charset', 'create_header', 
 51             'create_message_id', 'create_mime_multipart', 'create_mime_text', 
 52             'get_message_addresses', 'get_from_author', 'set_header'] 
 53   
 54   
 55  MAXHEADERLEN = 76 
 56  EMAIL_LOOKALIKE_PATTERN = ( 
 57          # the local part 
 58          r"[a-zA-Z0-9.'+_-]+" '@' 
 59          # the domain name part (RFC:1035) 
 60          '(?:[a-zA-Z0-9_-]+\.)+'  # labels (but also allow '_') 
 61          '[a-zA-Z](?:[-a-zA-Z\d]*[a-zA-Z\d])?'  # TLD 
 62          ) 
 63   
 64  _mime_encoding_re = re.compile(r'=\?[^?]+\?[bq]\?[^?]+\?=', re.IGNORECASE) 
 65   
 66  local_hostname = None 
67 68 69 -def create_charset(mime_encoding):
70 """Create an appropriate email charset for the given encoding. 71 72 Valid options are 'base64' for Base64 encoding, 'qp' for 73 Quoted-Printable, and 'none' for no encoding, in which case mails 74 will be sent as 7bit if the content is all ASCII, or 8bit otherwise. 75 """ 76 charset = Charset() 77 charset.input_charset = 'utf-8' 78 charset.output_charset = 'utf-8' 79 charset.input_codec = 'utf-8' 80 charset.output_codec = 'utf-8' 81 pref = mime_encoding.lower() 82 if pref == 'base64': 83 charset.header_encoding = BASE64 84 charset.body_encoding = BASE64 85 elif pref in ('qp', 'quoted-printable'): 86 charset.header_encoding = QP 87 charset.body_encoding = QP 88 elif pref == 'none': 89 charset.header_encoding = SHORTEST 90 charset.body_encoding = None 91 else: 92 raise TracError(_("Invalid email encoding setting: %(mime_encoding)s", 93 mime_encoding=mime_encoding)) 94 return charset
95
96 97 -def create_header(key, value, charset):
98 """Create an email Header. 99 100 The `key` is always a string and will be converted to the 101 appropriate `charset`. The `value` can either be a string or a 102 two-element tuple where the first item is the name and the 103 second item is the email address. 104 105 See `set_header()` for a helper that sets a header directly on a 106 message. 107 """ 108 maxlength = MAXHEADERLEN-(len(key)+2) 109 # Do not sent ridiculous short headers 110 if maxlength < 10: 111 raise TracError(_("Header length is too short")) 112 113 email = None 114 if isinstance(value, (tuple, list)): 115 value, email = value 116 if not isinstance(value, basestring): 117 value = to_unicode(value) 118 if not value: 119 return email 120 121 # when it matches mime-encoding, encode as mime even if only 122 # ascii characters 123 header = None 124 if not _mime_encoding_re.search(value): 125 try: 126 tmp = value.encode('ascii') 127 except UnicodeEncodeError: 128 pass 129 else: 130 header = Header(tmp, 'ascii', maxlinelen=maxlength) 131 if not header: 132 header = Header(value.encode(charset.output_codec), charset, 133 maxlinelen=maxlength) 134 header = str(header) 135 if email: 136 header = header.replace('\\', r'\\').replace('"', r'\"') 137 header = '"%s" <%s>' % (header, email) 138 return header
139
140 141 -def set_header(message, key, value, charset):
142 """Create and add or replace a header in a `MIMEMultipart`. 143 144 The `key` is always a string and will be converted to the 145 appropriate `charset`. The `value` can either be a string or a 146 two-element tuple where the first item is the name and the 147 second item is the email address. 148 149 The `charset` should be created using `create_charset()` 150 151 Example:: 152 153 set_header(my_message, 'From', ('Trac', '[email protected]'), 154 my_charset) 155 """ 156 header = create_header(key, value, charset) 157 if key in message: 158 message.replace_header(key, header) 159 else: 160 message[key] = header
161
162 163 -def create_mime_multipart(subtype):
164 """Create an email `MIMEMultipart`. 165 166 The `subtype` is a string that describes the type of multipart 167 message you are defining. You should pick one that is defined 168 by the email standards. The function does not check if the `subtype` 169 is valid. 170 171 The most common examples are: 172 173 * `related` infers that each part is in an integrated whole, like 174 images that are embedded in a html part. 175 * `alternative` infers that the message contains different formats 176 and the client can choose which to display based on capabilities 177 and user preferences, such as a text/html with an alternative 178 text/plain. 179 180 The `MIMEMultipart` is defined in the `email.mime.multipart` module in the 181 Python standard library. 182 """ 183 msg = MIMEMultipart(subtype) 184 del msg['Content-Transfer-Encoding'] 185 return msg
186
187 188 -def create_mime_text(body, format, charset):
189 """Create a `MIMEText` that can be added to an email message. 190 191 :param body: a string with the body of the message. 192 :param format: each text has a MIMEType, like `text/plain`. The 193 supertype is always `text`, so in the `format` parameter you 194 pass the subtype, like `plain` or `html`. 195 :param charset: should be created using `create_charset()`. 196 """ 197 if isinstance(body, unicode): 198 body = body.encode('utf-8') 199 msg = MIMEText(body, format) 200 # Message class computes the wrong type from MIMEText constructor, 201 # which does not take a Charset object as initializer. Reset the 202 # encoding type to force a new, valid evaluation 203 del msg['Content-Transfer-Encoding'] 204 msg.set_charset(charset) 205 return msg
206
207 208 -def create_message_id(env, targetid, from_email, time, more=None):
209 """Generate a predictable, but sufficiently unique message ID. 210 211 In case you want to set the "Message ID" header, this convenience 212 function will generate one by running a hash algorithm over a number 213 of properties. 214 215 :param env: the `Environment` 216 :param targetid: a string that identifies the target, like 217 `NotificationEvent.target` 218 :param from_email: the email address that the message is sent from 219 :param time: a Python `datetime` 220 :param more: a string that contains additional information that 221 makes this message unique 222 """ 223 items = [env.project_url.encode('utf-8'), targetid, to_utimestamp(time)] 224 if more is not None: 225 items.append(more.encode('ascii', 'ignore')) 226 source = '.'.join(str(item) for item in items) 227 hash_type = NotificationSystem(env).message_id_hash 228 try: 229 h = hashlib.new(hash_type) 230 except: 231 raise ConfigurationError(_("Unknown hash type '%(type)s'", 232 type=hash_type)) 233 h.update(source) 234 host = from_email[from_email.find('@') + 1:] 235 return '<%03d.%s@%s>' % (len(source), h.hexdigest(), host)
236
237 238 -def get_message_addresses(message, name):
239 return getaddresses(str(header) for header in message.get_all(name, ()))
240
241 242 -def get_from_author(env, event):
243 """Get the author name and email from a given `event`. 244 245 The `event` parameter should be of the type `NotificationEvent`. 246 If you only have the username of a Trac user, you should instead 247 use the `RecipientMatcher` to find the user's details. 248 249 The method returns a tuple that contains the name and email address 250 of the user. For example: `('developer', '[email protected]')`. 251 This tuple can be parsed by `set_header()`. 252 """ 253 if event.author and NotificationSystem(env).smtp_from_author: 254 matcher = RecipientMatcher(env) 255 from_ = matcher.match_from_author(event.author) 256 if from_: 257 return from_
258
259 260 -class RecipientMatcher(object):
261 """Matches user names and email addresses. 262 263 :param env: The `trac.env.Enviroment` 264 """ 265 nodomaddr_re = re.compile(r"^[-A-Za-z0-9!*+/=_.]+$") 266
267 - def __init__(self, env):
268 self.env = env 269 addrfmt = EMAIL_LOOKALIKE_PATTERN 270 self.notify_sys = NotificationSystem(env) 271 admit_domains = self.notify_sys.admit_domains_list 272 if admit_domains: 273 localfmt, domainfmt = addrfmt.split('@') 274 domains = [domainfmt] 275 domains.extend(re.escape(x) for x in admit_domains) 276 addrfmt = r'%s@(?:%s)' % (localfmt, '|'.join(domains)) 277 self.shortaddr_re = re.compile(r'<?(%s)>?$' % addrfmt, re.IGNORECASE) 278 self.longaddr_re = re.compile(r'(.*)\s+<\s*(%s)\s*>$' % addrfmt, 279 re.IGNORECASE) 280 self.ignore_domains = set(x.lower() 281 for x in self.notify_sys.ignore_domains_list)
282 283 @lazy
284 - def use_short_addr(self):
285 return self.notify_sys.use_short_addr
286 287 @lazy
288 - def smtp_default_domain(self):
289 return self.notify_sys.smtp_default_domain
290 291 @lazy
292 - def users(self):
293 return self.env.get_known_users(as_dict=True)
294
295 - def is_email(self, address):
296 """Check if an email address is valid. 297 298 This method checks against the list of domains that are 299 to be ignored, which is controlled by the `ignore_domains_list` 300 configuration option. 301 302 :param address: the address to validate 303 :return: `True` if it is a valid email address that is not in 304 the ignore list. 305 """ 306 if not address: 307 return False 308 match = self.shortaddr_re.match(address) 309 if match: 310 domain = address[address.find('@') + 1:].lower() 311 if domain not in self.ignore_domains: 312 return True 313 return False
314
315 - def match_recipient(self, address):
316 """Convenience function to check for an email address 317 318 The parameter `address` can either be a valid user name, 319 or an email address. The method first checks if the parameter 320 is a valid user name. If so, it will look up the address. If 321 there is no match, the function will check if it is a valid 322 email address. 323 324 :return: A tuple with a session id, a `1` or `0` to indicate 325 whether the user is authenticated, and the matched address. 326 Returns `None` when `address` does not match a valid user, 327 nor a valid email address. When `address` is an email address, 328 the sid will be `None` and the authentication parameter 329 will always be `0` 330 """ 331 if not address or address == 'anonymous': 332 return None 333 334 if address in self.users: 335 sid = address 336 auth = 1 337 address = (self.users[address][1] or '').strip() or sid 338 else: 339 sid = None 340 auth = 0 341 address = address.strip() 342 343 if self.nodomaddr_re.match(address): 344 if self.use_short_addr: 345 return sid, auth, address 346 if self.smtp_default_domain: 347 address = "%s@%s" % (address, self.smtp_default_domain) 348 return sid, auth, address 349 self.env.log.debug("Email address w/o domain: %s", address) 350 return None 351 352 mo = self.shortaddr_re.match(address) 353 if mo: 354 address = mo.group(1) 355 else: 356 mo = self.longaddr_re.match(address) 357 if mo: 358 address = mo.group(2) 359 if not self.is_email(address): 360 self.env.log.debug("Invalid email address: %s", address) 361 return None 362 return sid, auth, address
363
364 - def match_from_author(self, author):
365 """Find a name and email address for a specific user 366 367 :param author: The username that you want to query. 368 :return: On success, a two-item tuple is returned, with the 369 real name and the email address of the user. 370 """ 371 if author: 372 author = author.strip() 373 recipient = self.match_recipient(author) 374 if not recipient: 375 return None 376 sid, authenticated, address = recipient 377 if not address: 378 return None 379 from_name = None 380 if sid and authenticated and sid in self.users: 381 from_name = self.users[sid][0] 382 if not from_name: 383 mo = self.longaddr_re.match(author) 384 if mo: 385 from_name = mo.group(1) 386 return (from_name, address) if from_name else address
387
388 389 -class EmailDistributor(Component):
390 """Distributes notification events as emails.""" 391 392 implements(INotificationDistributor) 393 394 formatters = ExtensionPoint(INotificationFormatter) 395 decorators = ExtensionPoint(IEmailDecorator) 396 397 resolvers = OrderedExtensionsOption('notification', 398 'email_address_resolvers', IEmailAddressResolver, 399 'SessionEmailResolver', 400 include_missing=False, 401 doc="""Comma separated list of email resolver components in the order 402 they will be called. If an email address is resolved, the remaining 403 resolvers will not be called. 404 """) 405 406 default_format = Option('notification', 'default_format.email', 407 'text/plain', doc="Default format to distribute email notifications.") 408
409 - def __init__(self):
410 self._charset = create_charset(self.config.get('notification', 411 'mime_encoding'))
412 413 # INotificationDistributor methods 414
415 - def transports(self):
416 yield 'email'
417
418 - def distribute(self, transport, recipients, event):
419 if transport != 'email': 420 return 421 if not self.config.getbool('notification', 'smtp_enabled'): 422 self.log.debug("%s skipped because smtp_enabled set to false", 423 self.__class__.__name__) 424 return 425 426 formats = {} 427 for f in self.formatters: 428 for style, realm in f.get_supported_styles(transport): 429 if realm == event.realm: 430 formats[style] = f 431 if not formats: 432 self.log.error("%s No formats found for %s %s", 433 self.__class__.__name__, transport, event.realm) 434 return 435 self.log.debug("%s has found the following formats capable of " 436 "handling '%s' of '%s': %s", self.__class__.__name__, 437 transport, event.realm, ', '.join(formats.keys())) 438 439 matcher = RecipientMatcher(self.env) 440 notify_sys = NotificationSystem(self.env) 441 always_cc = set(notify_sys.smtp_always_cc_list) 442 addresses = {} 443 for sid, auth, addr, fmt in recipients: 444 if fmt not in formats: 445 self.log.debug("%s format %s not available for %s %s", 446 self.__class__.__name__, fmt, transport, 447 event.realm) 448 continue 449 450 if sid and not addr: 451 for resolver in self.resolvers: 452 addr = resolver.get_address_for_session(sid, auth) or None 453 if addr: 454 self.log.debug( 455 "%s found the address '%s' for '%s [%s]' via %s", 456 self.__class__.__name__, addr, sid, auth, 457 resolver.__class__.__name__) 458 break 459 if sid and auth and not addr: 460 addr = sid 461 if notify_sys.smtp_default_domain and \ 462 not notify_sys.use_short_addr and \ 463 addr and matcher.nodomaddr_re.match(addr): 464 addr = '%s@%s' % (addr, notify_sys.smtp_default_domain) 465 if not addr: 466 self.log.debug("%s was unable to find an address for " 467 "'%s [%s]'", self.__class__.__name__, sid, auth) 468 elif matcher.is_email(addr) or \ 469 notify_sys.use_short_addr and \ 470 matcher.nodomaddr_re.match(addr): 471 addresses.setdefault(fmt, set()).add(addr) 472 if sid and auth and sid in always_cc: 473 always_cc.discard(sid) 474 always_cc.add(addr) 475 elif notify_sys.use_public_cc: 476 always_cc.add(addr) 477 else: 478 self.log.debug("%s was unable to use an address '%s' for '%s " 479 "[%s]'", self.__class__.__name__, addr, sid, 480 auth) 481 482 outputs = {} 483 failed = [] 484 for fmt, formatter in formats.iteritems(): 485 if fmt not in addresses and fmt != 'text/plain': 486 continue 487 try: 488 outputs[fmt] = formatter.format(transport, fmt, event) 489 except Exception as e: 490 self.log.warn('%s caught exception while ' 491 'formatting %s to %s for %s: %s%s', 492 self.__class__.__name__, event.realm, fmt, 493 transport, formatter.__class__, 494 exception_to_unicode(e, traceback=True)) 495 failed.append(fmt) 496 497 # Fallback to text/plain when formatter is broken 498 if failed and 'text/plain' in outputs: 499 for fmt in failed: 500 addresses.setdefault('text/plain', set()) \ 501 .update(addresses.pop(fmt, ())) 502 503 for fmt, addrs in addresses.iteritems(): 504 self.log.debug("%s is sending event as '%s' to: %s", 505 self.__class__.__name__, fmt, ', '.join(addrs)) 506 message = self._create_message(fmt, outputs) 507 if message: 508 addrs = set(addrs) 509 cc_addrs = sorted(addrs & always_cc) 510 bcc_addrs = sorted(addrs - always_cc) 511 self._do_send(transport, event, message, cc_addrs, bcc_addrs) 512 else: 513 self.log.warn("%s cannot send event '%s' as '%s': %s", 514 self.__class__.__name__, event.realm, fmt, 515 ', '.join(addrs))
516
517 - def _create_message(self, format, outputs):
518 if format not in outputs: 519 return None 520 message = create_mime_multipart('related') 521 maintype, subtype = format.split('/') 522 preferred = create_mime_text(outputs[format], subtype, self._charset) 523 if format != 'text/plain' and 'text/plain' in outputs: 524 alternative = create_mime_multipart('alternative') 525 alternative.attach(create_mime_text(outputs['text/plain'], 526 'plain', self._charset)) 527 alternative.attach(preferred) 528 preferred = alternative 529 message.attach(preferred) 530 return message
531
532 - def _do_send(self, transport, event, message, cc_addrs, bcc_addrs):
533 notify_sys = NotificationSystem(self.env) 534 smtp_from = notify_sys.smtp_from 535 smtp_from_name = notify_sys.smtp_from_name or self.env.project_name 536 smtp_replyto = notify_sys.smtp_replyto 537 if not notify_sys.use_short_addr and notify_sys.smtp_default_domain: 538 if smtp_from and '@' not in smtp_from: 539 smtp_from = '%s@%s' % (smtp_from, 540 notify_sys.smtp_default_domain) 541 if smtp_replyto and '@' not in smtp_replyto: 542 smtp_replyto = '%s@%s' % (smtp_replyto, 543 notify_sys.smtp_default_domain) 544 545 headers = {} 546 headers['X-Mailer'] = 'Trac %s, by Edgewall Software'\ 547 % self.env.trac_version 548 headers['X-Trac-Version'] = self.env.trac_version 549 headers['X-Trac-Project'] = self.env.project_name 550 headers['X-URL'] = self.env.project_url 551 headers['X-Trac-Realm'] = event.realm 552 headers['Precedence'] = 'bulk' 553 headers['Auto-Submitted'] = 'auto-generated' 554 if isinstance(event.target, (list, tuple)): 555 targetid = ','.join(map(get_target_id, event.target)) 556 else: 557 targetid = get_target_id(event.target) 558 rootid = create_message_id(self.env, targetid, smtp_from, None, 559 more=event.realm) 560 if event.category == 'created': 561 headers['Message-ID'] = rootid 562 else: 563 headers['Message-ID'] = create_message_id(self.env, targetid, 564 smtp_from, event.time, 565 more=event.realm) 566 headers['In-Reply-To'] = rootid 567 headers['References'] = rootid 568 headers['Date'] = formatdate() 569 headers['From'] = (smtp_from_name, smtp_from) \ 570 if smtp_from_name else smtp_from 571 headers['To'] = 'undisclosed-recipients: ;' 572 if cc_addrs: 573 headers['Cc'] = ', '.join(cc_addrs) 574 if bcc_addrs: 575 headers['Bcc'] = ', '.join(bcc_addrs) 576 headers['Reply-To'] = smtp_replyto 577 578 for k, v in headers.iteritems(): 579 set_header(message, k, v, self._charset) 580 for decorator in self.decorators: 581 decorator.decorate_message(event, message, self._charset) 582 583 from_name, from_addr = parseaddr(str(message['From'])) 584 to_addrs = set() 585 for name in ('To', 'Cc', 'Bcc'): 586 values = map(str, message.get_all(name, ())) 587 to_addrs.update(addr for name, addr in getaddresses(values) 588 if addr) 589 del message['Bcc'] 590 notify_sys.send_email(from_addr, list(to_addrs), message.as_string())
591
592 593 -class SmtpEmailSender(Component):
594 """E-mail sender connecting to an SMTP server.""" 595 596 implements(IEmailSender) 597 598 smtp_server = Option('notification', 'smtp_server', 'localhost', 599 """SMTP server hostname to use for email notifications.""") 600 601 smtp_port = IntOption('notification', 'smtp_port', 25, 602 """SMTP server port to use for email notification.""") 603 604 smtp_user = Option('notification', 'smtp_user', '', 605 """Username for authenticating with SMTP server.""") 606 607 smtp_password = Option('notification', 'smtp_password', '', 608 """Password for authenticating with SMTP server.""") 609 610 use_tls = BoolOption('notification', 'use_tls', 'false', 611 """Use SSL/TLS to send notifications over SMTP.""") 612
613 - def send(self, from_addr, recipients, message):
614 global local_hostname 615 # Ensure the message complies with RFC2822: use CRLF line endings 616 message = fix_eol(message, CRLF) 617 618 self.log.info("Sending notification through SMTP at %s:%d to %s", 619 self.smtp_server, self.smtp_port, recipients) 620 try: 621 server = smtplib.SMTP(self.smtp_server, self.smtp_port, 622 local_hostname) 623 local_hostname = server.local_hostname 624 except smtplib.socket.error as e: 625 raise ConfigurationError( 626 tag_("SMTP server connection error (%(error)s). Please " 627 "modify %(option1)s or %(option2)s in your " 628 "configuration.", 629 error=to_unicode(e), 630 option1=tag.code("[notification] smtp_server"), 631 option2=tag.code("[notification] smtp_port"))) 632 # server.set_debuglevel(True) 633 if self.use_tls: 634 server.ehlo() 635 if 'starttls' not in server.esmtp_features: 636 raise TracError(_("TLS enabled but server does not support" 637 " TLS")) 638 server.starttls() 639 server.ehlo() 640 if self.smtp_user: 641 server.login(self.smtp_user.encode('utf-8'), 642 self.smtp_password.encode('utf-8')) 643 start = time_now() 644 server.sendmail(from_addr, recipients, message) 645 t = time_now() - start 646 if t > 5: 647 self.log.warning("Slow mail submission (%.2f s), " 648 "check your mail setup", t) 649 if self.use_tls: 650 # avoid false failure detection when the server closes 651 # the SMTP connection with TLS enabled 652 import socket 653 try: 654 server.quit() 655 except socket.sslerror: 656 pass 657 else: 658 server.quit()
659
660 661 -class SendmailEmailSender(Component):
662 """E-mail sender using a locally-installed sendmail program.""" 663 664 implements(IEmailSender) 665 666 sendmail_path = Option('notification', 'sendmail_path', 'sendmail', 667 """Path to the sendmail executable. 668 669 The sendmail program must accept the `-i` and `-f` options. 670 (''since 0.12'')""") 671
672 - def send(self, from_addr, recipients, message):
673 # Use native line endings in message 674 message = fix_eol(message, os.linesep) 675 676 self.log.info("Sending notification through sendmail at %s to %s", 677 self.sendmail_path, recipients) 678 cmdline = [self.sendmail_path, '-i', '-f', from_addr] + recipients 679 self.log.debug("Sendmail command line: %s", cmdline) 680 try: 681 child = Popen(cmdline, bufsize=-1, stdin=PIPE, stdout=PIPE, 682 stderr=PIPE, close_fds=close_fds) 683 except OSError as e: 684 raise ConfigurationError( 685 tag_("Sendmail error (%(error)s). Please modify %(option)s " 686 "in your configuration.", 687 error=to_unicode(e), 688 option=tag.code("[notification] sendmail_path"))) 689 out, err = child.communicate(message) 690 if child.returncode or err: 691 raise Exception("Sendmail failed with (%s, %s), command: '%s'" 692 % (child.returncode, err.strip(), cmdline))
693
694 695 -class SessionEmailResolver(Component):
696 """Gets the email address from the user preferences / session.""" 697 698 implements(IEmailAddressResolver) 699
700 - def get_address_for_session(self, sid, authenticated):
701 return get_session_attribute(self.env, sid, authenticated, 'email')
702
703 704 -class AlwaysEmailSubscriber(Component):
705 """Implement a policy to -always- send an email to a certain address. 706 707 Controlled via the smtp_always_cc and smtp_always_bcc option in the 708 notification section of trac.ini. 709 """ 710 711 implements(INotificationSubscriber) 712
713 - def matches(self, event):
714 matcher = RecipientMatcher(self.env) 715 klass = self.__class__.__name__ 716 format = None 717 priority = 0 718 for address in self._get_address_list(): 719 recipient = matcher.match_recipient(address) 720 if recipient: 721 sid, authenticated, address = recipient 722 yield (klass, 'email', sid, authenticated, address, format, 723 priority, 'always')
724
725 - def description(self):
726 return None # not configurable
727
728 - def requires_authentication(self):
729 return False
730
731 - def default_subscriptions(self):
732 return ()
733
734 - def _get_address_list(self):
735 section = self.config['notification'] 736 def getlist(name): 737 return section.getlist(name, sep=(',', ' '), keep_empty=False)
738 return set(getlist('smtp_always_cc')) | \ 739 set(getlist('smtp_always_bcc'))
740
741 742 -class FromAuthorEmailDecorator(Component):
743 """Implement a policy to use the author of the event as the sender in 744 notification emails. 745 746 Controlled via the smtp_from_author option in the notification section 747 of trac.ini. 748 """ 749 750 implements(IEmailDecorator) 751
752 - def decorate_message(self, event, message, charset):
753 from_ = get_from_author(self.env, event) 754 if from_: 755 set_header(message, 'From', from_, charset)
756