Index: acct_mgr/notification.py
===================================================================
--- acct_mgr/notification.py	(revision 17453)
+++ acct_mgr/notification.py	(working copy)
@@ -8,15 +8,20 @@
 # you should have received as part of this distribution.
 #
 # Author: Pedro Algarvio <ufs@ufsoft.org>
+from email.utils import formatdate
 
 from trac.admin import IAdminPanelProvider
 from trac.config import Option, ListOption
 from trac.core import Component, TracError, implements
-from trac.notification import NotifyEmail
 
 from acct_mgr.api import IAccountChangeListener, CommonTemplateProvider, \
                          _, dgettext
 from acct_mgr.compat import genshi_template_args
+from trac.notification import NotificationSystem
+from trac.notification.mail import RecipientMatcher, set_header, create_charset, create_mime_multipart, create_mime_text
+from trac.ticket import translation_deactivated
+from trac.util import exception_to_unicode, lazy
+from trac.web.chrome import Chrome
 
 
 class NotificationError(TracError):
@@ -31,157 +36,160 @@
         doc="""Comma separated list of actions to notify of.
         Available actions 'new', 'change', 'delete'.""")
 
+    _recipients = ListOption(
+        'account-manager', 'account_changes_notify_addresses', [], sep=[' ', ','],
+        doc="""List of email addresses that get notified of user changes, ie,
+        new user, password change and delete user.""")
+
     # IAccountChangeListener methods
-
     def user_created(self, username, password):
         if 'new' in self._notify_actions:
-            notifier = AccountChangeNotification(self.env)
-            notifier.notify(username, 'New user registration')
+            self._send_notification_email(username, "New user registration")
 
     def user_password_changed(self, username, password):
         if 'change' in self._notify_actions:
-            notifier = AccountChangeNotification(self.env)
-            notifier.notify(username, 'Password reset')
+            self._send_notification_email(username, 'Password reset')
 
     def user_deleted(self, username):
         if 'delete' in self._notify_actions:
-            notifier = AccountChangeNotification(self.env)
-            notifier.notify(username, 'Deleted User')
+            self._send_notification_email(username, 'Deleted user')
 
     def user_password_reset(self, username, email, password):
-        notifier = PasswordResetNotification(self.env)
-        if email != notifier.email_map.get(username):
-            raise Exception(
-                _("The email and username do not match a known account."))
-        notifier.notify(username, password)
+        data = {
+            'account': {
+                'username': username,
+                'password': password,
+            },
+            'login': {
+                'link': self.env.abs_href.login(),
+            },
+        }
 
+        subject = '[%s] Trac password reset for user: %s' \
+                  % (self.env.project_name, username)
+        send_email(self.env, [username], subject,
+                   'account_reset_password_email.txt', data)
+
     def user_email_verification_requested(self, username, token):
-        notifier = EmailVerificationNotification(self.env)
-        notifier.notify(username, token)
+        data = {
+            'account': {
+                'username': username,
+                'token': token,
+            },
+            'verify': {
+                'link': self.env.abs_href.verify_email(token=token, verify=1),
+            },
+        }
 
+        subject = '[%s] Trac email verification for user: %s' \
+                  % (self.env.project_name, username)
+        send_email(self.env, [username], subject,
+                   'account_verify_email.txt', data)
+
     def user_registration_approval_required(self, username):
-        notifier = EmailVerificationNotification(self.env)
-        notifier.notify(username, 'Registration approval required')
+        # reuse the email verification template
+        self.user_email_verification_requested(username, 'Registration approval required')
 
-
-class AccountChangeNotification(NotifyEmail):
-    template_name = 'account_user_changes_email.txt'
-
-    _recipients = Option(
-        'account-manager', 'account_changes_notify_addresses', '',
-        """List of email addresses that get notified of user changes, ie,
-        new user, password change and delete user.""")
-
-    def get_recipients(self, resid):
-        recipients = self._recipients.split()
-        return recipients, []
-
-    def get_smtp_address(self, addr):
-        """Overrides `get_smtp_address` in order to prevent CCing users
-        other than those in the account_changes_notify_addresses option.
-        """
-        if addr in self._recipients:
-            return NotifyEmail.get_smtp_address(self, addr)
-        else:
-            return
-
-    def notify(self, username, action):
-        self.data.update({
+    # Helper method
+    def _send_notification_email(self, username, action):
+        data = {
             'account': {
                 'username': username,
                 'action': action
             },
             'login': {
-                'link': self.env.abs_href.login(),
-            }
-        })
+                'link': self.env.abs_href.login()
+            },
+        }
 
-        projname = self.config.get('project', 'name')
-        subject = '[%s] %s: %s' % (projname, action, username)
+        subject = '[%s] %s: %s' % (self.env.project_name, action, username)
+        send_email(self.env, self._recipients, subject,
+                   'account_user_changes_email.txt', data)
 
-        try:
-            NotifyEmail.notify(self, username, subject)
-        except Exception, e:
-            # Enable dedicated, graceful handling of notification issues.
-            raise NotificationError(e)
 
-
-class SingleUserNotification(NotifyEmail):
-    """Helper class used for account email notifications which should only be
-    sent to one persion, not including the rest of the normally CCed users
+def send_email(env, recipients, subject, template_name, data):
+    """Helper function that can be used to build an email that is addressed to
+    exactly one person.
+    :param env: a Trac `Environment`
+    :param recipients: a list of recipients to send a message to. A recipient
+        either is a trac user name, an email address, or a valid shorthand
+        address
+    :param subject: a string that is used as subject of the message
+    :param template_name: the name of a Genshi text template
+    :param data: the data to pass to Genshi
     """
-    _username = None
 
-    def get_recipients(self, resid):
-        return [resid], []
+    # Determine the email charset
+    charset = create_charset(env.config.get('notification', 'mime_encoding'))
 
-    def get_smtp_address(self, addr):
-        """Overrides `get_smtp_address` in order to prevent CCing users
-        other than the user whose password is being reset.
-        """
-        if addr == self._username:
-            return NotifyEmail.get_smtp_address(self, addr)
-        else:
-            return
+    # Process the recipient list
+    if not isinstance(recipients, list) or len(recipients) == 0:
+        raise NotificationError("send_email(): Invalid recipient list")
+    matcher = RecipientMatcher(env)
+    processed_recipients = []
+    for recipient in recipients:
+        address_result = matcher.match_recipient(recipient)
+        if address_result is None:
+            raise NotificationError("send_email(): invalid recipient")
+        _, _, address = address_result
+        processed_recipients.append(address)
 
-    def notify(self, username, subject):
-        # save the username for use in `get_smtp_address`
-        self._username = username
-        old_public_cc = self.config.getbool('notification', 'use_public_cc')
-        # override public cc option so that the user's email is included in
-        # the To: field
-        self.config.set('notification', 'use_public_cc', 'true')
-        try:
-            NotifyEmail.notify(self, username, subject)
-        except Exception, e:
-            raise NotificationError(e)
-        # DEVEL: Better use new 'finally' statement here, but
-        #   still need to care for Python 2.4 (RHEL5.x) for now
-        self.config.set('notification', 'use_public_cc', old_public_cc)
+    # The following is graciously taken/borrowed from trac.notification.mail._do_send()
+    # Unfortunately it is not generic code right now and therefore cannot be reused.
+    notify_sys = NotificationSystem(env)
+    smtp_from = notify_sys.smtp_from
+    smtp_from_name = notify_sys.smtp_from_name or env.project_name
+    smtp_replyto = notify_sys.smtp_replyto
 
+    if not notify_sys.use_short_addr and notify_sys.smtp_default_domain:
+        if smtp_from and '@' not in smtp_from:
+            smtp_from = '%s@%s' % (smtp_from,
+                                   notify_sys.smtp_default_domain)
+        if smtp_replyto and '@' not in smtp_replyto:
+            smtp_replyto = '%s@%s' % (smtp_replyto,
+                                      notify_sys.smtp_default_domain)
 
-class PasswordResetNotification(SingleUserNotification):
-    template_name = 'account_reset_password_email.txt'
+    headers = {}
+    headers['X-Mailer'] = 'Trac %s, by Edgewall Software' \
+                          % env.trac_version
+    headers['X-Trac-Version'] = env.trac_version
+    headers['X-Trac-Project'] = env.project_name
+    headers['X-URL'] = env.project_url
+    headers['X-Trac-Realm'] = "AccountManagerPlugin"
+    headers['Precedence'] = 'bulk'
+    headers['Auto-Submitted'] = 'auto-generated'
+    headers['Date'] = formatdate()
+    headers['From'] = (smtp_from_name, smtp_from) \
+        if smtp_from_name else smtp_from
+    headers['To'] = ", ".join(processed_recipients)
+    headers['Reply-To'] = smtp_replyto
+    headers['Subject'] = subject
 
-    def notify(self, username, password):
-        self.data.update({
-            'account': {
-                'username': username,
-                'password': password,
-            },
-            'login': {
-                'link': self.env.abs_href.login(),
-            }
-        })
+    # Build the message. We use the message tools from trac.notification.email
+    message_format = "text/plain"
+    chrome = Chrome(env)
+    template = chrome.load_template(template_name, text=True)
+    data = chrome.populate_data(None, data)
+    with translation_deactivated():
+        try:
+            message_body = chrome.render_template_string(template, data, text=True).encode('utf-8')
+        except Exception as e:
+            env.log.debug("Failed to format body of notification mail: %s",
+                           exception_to_unicode(e, traceback=True))
+            raise e
 
-        projname = self.config.get('project', 'name')
-        subject = '[%s] Trac password reset for user: %s' \
-                  % (projname, username)
+    message = create_mime_multipart('alternative')
+    maintype, subtype = message_format.split('/')
+    message_mime_text = create_mime_text(message_body, subtype, charset)
+    message.attach(message_mime_text)
 
-        SingleUserNotification.notify(self, username, subject)
+    # Set up the message headers
+    for k, v in headers.iteritems():
+        set_header(message, k, v, charset)
 
+    notify_sys.send_email(smtp_from, processed_recipients, message.as_string())
 
-class EmailVerificationNotification(SingleUserNotification):
-    template_name = 'account_verify_email.txt'
 
-    def notify(self, username, token):
-        self.data.update({
-            'account': {
-                'username': username,
-                'token': token,
-            },
-            'verify': {
-                'link': self.env.abs_href.verify_email(token=token, verify=1),
-            }
-        })
-
-        proj_name = self.config.get('project', 'name')
-        subject = '[%s] Trac email verification for user: %s' \
-                  % (proj_name, username)
-
-        SingleUserNotification.notify(self, username, subject)
-
-
 class AccountChangeNotificationAdminPanel(CommonTemplateProvider):
     implements(IAdminPanelProvider)
 
