1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
|
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 Pedro Algarvio <ufs@ufsoft.org>
# Copyright (C) 2013-2015 Steffen Hoffmann <hoff.st@web.de>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.
#
# Author: Pedro Algarvio <ufs@ufsoft.org>
from trac.admin.api import IAdminPanelProvider
from trac.config import ListOption
from trac.core import Component, TracError, implements
from trac.notification.api import (
IEmailDecorator, INotificationFormatter, NotificationEvent,
NotificationSystem)
from trac.notification.mail import RecipientMatcher, set_header
from trac.util.text import exception_to_unicode
from trac.util.translation import deactivate, dgettext, reactivate
from trac.web.chrome import Chrome
from .api import IAccountChangeListener, _
from .compat import iteritems, use_jinja2
from .util import i18n_tag
class NotificationError(TracError):
pass
class AccountChangeEvent(NotificationEvent):
realm = 'account'
def __init__(self, category, username, data):
super(AccountChangeEvent, self).__init__(self.realm, category, None,
None, username)
self.data = data
class AccountChangeListener(Component):
implements(IAccountChangeListener)
_notify_actions = ListOption(
'account-manager', 'notify_actions', [],
doc="""Comma separated list of notification actions. Available
actions are 'new', 'change', 'delete'.
""")
_account_change_recipients = ListOption(
'account-manager', 'account_changes_notify_addresses', [],
doc="""Email addresses to notify on account created, password
changed and account deleted.
""")
action_category_map = {
'new': 'created',
'change': 'password changed',
'delete': 'deleted'
}
def __init__(self):
self._notify_categories = [category
for action, category
in iteritems(self.action_category_map)
if action in self._notify_actions]
# IAccountChangeListener methods
def user_created(self, username, password):
data = {'password': password}
self._send_notification('created', username, data)
def user_password_changed(self, username, password):
data = {'password': password}
self._send_notification('password changed', username, data)
def user_deleted(self, username):
self._send_notification('deleted', username)
def user_password_reset(self, username, email, password):
data = {'password': password, 'email': email}
self._send_notification('password reset', username, data)
def user_email_verification_requested(self, username, token):
data = {'token': token}
self._send_notification('verify email', username, data)
def user_registration_approval_required(self, username):
self._send_notification('verify email', username)
# Helper method
def _send_notification(self, category, username, data=None):
event = AccountChangeEvent(category, username, data)
subscriptions = self._subscriptions(event)
try:
NotificationSystem(self.env).distribute_event(event, subscriptions)
except Exception as e:
self.log.error("Failure sending notification for '%s' for user "
"%s: %s", category, username,
exception_to_unicode(e))
raise NotificationError(e)
def _subscriptions(self, event):
matcher = RecipientMatcher(self.env)
transport_and_format = ('email', 'text/plain')
if event.category in ('verify email', 'password reset'):
recipient = matcher.match_recipient(event.author)
if recipient:
yield recipient + transport_and_format
elif event.category in self._notify_categories:
for r in self._account_change_recipients:
recipient = matcher.match_recipient(r)
if recipient:
yield recipient + transport_and_format
class AccountNotificationFormatter(Component):
implements(IEmailDecorator, INotificationFormatter)
realm = 'account'
# IEmailDecorator methods
def decorate_message(self, event, message, charset):
if event.realm != self.realm:
return
# Someday replace with method added in trac:#13208
prefix = self.config.get('notification', 'smtp_subject_prefix')
subject = '[%s]' % self.env.project_name \
if prefix == '__default__' else prefix
if event.category in ('created', 'password changed', 'deleted'):
subject += " Account %s: %s" % (event.category, event.author)
elif event.category == 'password reset':
subject += " Account password reset: %s" % event.author
elif event.category == 'verify email':
subject += " Account email verification: %s" % event.author
set_header(message, 'Subject', subject, charset)
# INotificationFormatter methods
def get_supported_styles(self, transport):
yield 'text/plain', self.realm
def format(self, transport, style, event):
if event.realm != self.realm:
return
data = {
'account': {'username': event.author},
'login': {'link': self.env.abs_href.login()},
}
if event.category in ('created', 'password changed', 'deleted'):
data['account']['action'] = event.category
template_name = 'account_user_changes_email.txt'
elif event.category == 'password reset':
data['account']['password'] = event.data['password']
template_name = 'account_reset_password_email.txt'
elif event.category == 'verify email':
token = event.data['token']
data['account']['token'] = token
data['verify'] = {
'link': self.env.abs_href.verify_email(token=token, verify=1)
}
template_name = 'account_verify_email.txt'
t = deactivate() # don't translate the e-mail stream
try:
return self._format_body(data, template_name)
finally:
reactivate(t)
# Internal methods
def _format_body(self, data, template_name):
chrome = Chrome(self.env)
data = chrome.populate_data(None, data)
if use_jinja2:
template = chrome.load_template(template_name, text=True)
body = chrome.render_template_string(template, data, text=True)
return body.encode('utf-8')
else:
template = chrome.load_template(template_name, method='text')
stream = template.generate(**data)
return stream.render('text', encoding='utf-8')
class AccountChangeNotificationAdminPanel(Component):
implements(IAdminPanelProvider)
# IAdminPageProvider methods
def get_admin_panels(self, req):
if 'ACCTMGR_CONFIG_ADMIN' in req.perm:
yield ('accounts', _("Accounts"), 'notification',
_("Notification"))
def render_admin_panel(self, req, cat, page, path_info):
if page == 'notification':
return self._do_config(req)
def _do_config(self, req):
cfg = self.config['account-manager']
if req.method == 'POST':
cfg.set('account_changes_notify_addresses',
' '.join(req.args.getlist('notify_addresses')))
cfg.set('notify_actions',
','.join(req.args.getlist('notify_actions')))
self.config.save()
req.redirect(req.href.admin('accounts', 'notification'))
notify_addresses = cfg.getlist('account_changes_notify_addresses',
sep=' ')
notify_actions = cfg.getlist('notify_actions')
data = {
'i18n_tag': i18n_tag,
'notify_actions': notify_actions,
'notify_addresses': notify_addresses
}
return 'account_notification.html', data
|