Source code for adhocracy_core.messaging

"""Send messages to Principals."""
from copy import copy
from collections.abc import Sequence
from logging import getLogger
from urllib.request import quote

from pyramid.registry import Registry
from pyramid.renderers import render
from pyramid_mailer.interfaces import IMailer
from pyramid_mailer.message import Message
from pyramid.traversal import resource_path
from pyramid.request import Request
from pyramid.threadlocal import get_current_request
from pyramid.i18n import TranslationStringFactory
from pyramid.i18n import get_localizer

from adhocracy_core.activity import generate_activity_description
from adhocracy_core.interfaces import Activity
from adhocracy_core.interfaces import ActivityType
from adhocracy_core.interfaces import IResource
from adhocracy_core.resources.principal import IUser
from adhocracy_core.sheets.principal import IUserBasic
from adhocracy_core.sheets.principal import IUserExtended

logger = getLogger(__name__)

_ = TranslationStringFactory('adhocracy')


[docs]class Messenger: """Send messages to other people.""" def __init__(self, registry: Registry): """Create a new instance. :param registry: used to retrieve and configure the mailer """ self.registry = registry settings = registry['config'] self.settings = settings self.abuse_handler_mail = settings.adhocracy.abuse_handler_mail self.site_name = settings.adhocracy.site_name self.frontend_url = settings.adhocracy.canonical_url self.mailer = registry.getUtility(IMailer)
[docs] def send_mail(self, subject: str, recipients: Sequence, sender: str=None, body: str=None, html: str=None, request: Request=None, ): """Send a mail message to a list of recipients. :param subject: the subject of the message :param recipients: non-empty list of the email addresses of recipients :param sender: the email message of the sender; if None, the configured default sender address will be used :param body: the plain text body of the message, may be omitted but only if ``html`` is given :param html: body: the HTML body of the message, may be omitted but only if ``body`` is given :param request: the current request object, if None pyramid.threadlocal.get_current_request is used :raise ValueError: if ``recipients`` is empty or if both ``body`` and ``html`` are missing or empty :raise ConnectionError: if no connection to the configured mail server can be established :raise smtplib.SMTPException: if the mail cannot be sent because the target mail server doesn't exist or rejects the connection """ if not recipients: raise ValueError('Empty list of recipients') if not (body or html): raise ValueError('Email has neither body nor html') request = request or get_current_request() if request: # ease testing translate = get_localizer(request).translate subject = subject and translate(subject) body = body and translate(body) html = html and translate(html) message = Message(subject=subject, sender=sender, recipients=recipients, body=body, html=html, ) debug_msg = 'Sending message "{0}" from {1} to {2} with body:\n{3}' logger.debug(debug_msg.format(subject, sender, recipients, body)) if self.settings.adhocracy.use_mail_queue: self.mailer.send_to_queue(message) else: self.mailer.send_immediately(message)
[docs] def send_abuse_complaint(self, url: str, remark: str, user: IResource=None, request: Request=None): """Send an abuse complaint to the preconfigured abuse handler. :param url: the frontend URL of the resource considered offensive :param remark: explanation provided by the complaining user :param user: the complaining user, or `None` if not logged in """ user_name = None user_url = None if user is not None: user_name = self._get_user_name(user) user_url = self._get_user_url(user) mapping = {'url': url, 'remark': remark, 'user_name': user_name, 'user_url': user_url, 'site_name': self.site_name, } subject = _('mail_abuse_complaint_subject', mapping=mapping, default='${site_name} Abuse Complaint') body = render('adhocracy_core:templates/abuse_complaint.txt.mako', mapping) # FIXME For security reasons, we should check that the url starts # with one of the prefixes where frontends are supposed to be running self.send_mail(subject=subject, recipients=[self.abuse_handler_mail], body=body, request=request, )
[docs] def send_message_to_user(self, recipient: IResource, title: str, text: str, from_user: IResource, request: Request=None, ): """Send a message to a specific user.""" settings = self.registry['config'].configurator from_email = settings.mail.noreply_sender recipient_email = self._get_user_email(recipient) sender_name = self._get_user_name(from_user) sender_url = self._get_user_url(from_user) mapping = {'site_name': self.site_name, 'sender_name': sender_name, 'sender_url': sender_url, 'title': title, 'text': text} subject = _('mail_sent_message_to_user_subject', mapping=mapping, default='${site_name} Message from ${sender_name}: ' '${title}') body = _('mail_sent_message_to_user_body_txt', mapping=mapping, default='${text}') self.send_mail( subject=subject, recipients=[recipient_email], body=body, sender=from_email, request=request, )
def _get_user_email(self, user: IResource) -> str: return self.registry.content.get_sheet_field(user, IUserExtended, 'email') def _get_user_name(self, user: IResource) -> str: return self.registry.content.get_sheet_field(user, IUserBasic, 'name') def _get_user_url(self, user: IResource) -> str: path = resource_path(user) return '%s/r%s/' % (self.frontend_url, path)
[docs] def send_registration_mail(self, user: IUser, activation_path: str, request: Request=None): """Send a registration mail to validate the email of a user account.""" mapping = {'activation_path': activation_path, 'frontend_url': self.frontend_url, 'user_name': user.name, 'site_name': self.site_name, } subject = _('mail_account_verification_subject', mapping=mapping, default='${site_name}: Account Verification / ' 'Aktivierung Deines Nutzerkontos') body_txt = _('mail_account_verification_body_txt', mapping=mapping, default='${frontend_url}${activation_path}') self.send_mail(subject=subject, recipients=[user.email], body=body_txt, request=request, )
[docs] def send_password_reset_mail(self, user=IUser, password_reset=IResource, request: Request=None): """Send email with link to reset the user password.""" mapping = {'reset_url': self._build_reset_url(password_reset), 'user_name': user.name, 'site_name': self.site_name, } subject = _('mail_reset_password_subject', mapping=mapping, default='${site_name}: Reset Password / Passwort neu' ' setzen') body = _('mail_reset_password_body_txt', mapping=mapping, default='${reset_url}' ) self.send_mail(subject=subject, recipients=[user.email], body=body, request=request, )
[docs] def send_invitation_mail(self, user=IUser, password_reset=IResource, request: Request=None, subject_tmpl: str=None, body_tmpl: str=None, ): """Send invitation email with link to reset the user password. :param subject_tmpl: pyramid asset pointing to a mako template file. If not set the translation message is used for the mail subject. :param body_tmpl: pyramid asset pointing to a mako template file. If not set the translation message is used for the mail body. """ mapping = {'reset_url': self._build_reset_url(password_reset), 'user_name': user.name, 'site_name': self.site_name, 'email': user.email, } if subject_tmpl is None: subject = _('mail_invitation_subject', mapping=mapping, default='Invitation to join ${site_name}') else: # remove potential newline, as it causes invalid headers subject = render(subject_tmpl, mapping).rstrip() if body_tmpl is None: body = _('mail_invitation_body_txt', mapping=mapping, default='Please click ${reset_url} to join' ' and reset your password.' ) else: body = render(body_tmpl, mapping) self.send_mail(subject=subject, recipients=[user.email], body=body, request=request, )
def _build_reset_url(self, reset: IResource) -> str: reset_path = resource_path(reset) reset_path_quoted = quote(reset_path, safe='') reset_url = '{0}/password_reset/?path={1}'.format(self.frontend_url, reset_path_quoted) return reset_url
[docs] def send_activity_mail(self, user: IUser, activity: Activity, request: Request, ): """Send email describing an activity event. The following variables are provided for translations strings: - `site_name` - `object_url` - `target_url` - `activity_name` - `activity_description` - `object_type_name` - `target_type_name` """ translate = get_localizer(request).translate description = generate_activity_description(activity, request) mapping = copy(description.mapping) mapping.update({'site_name': self.site_name, 'activity_name': translate(activity.name), 'activity_description': translate(description), 'object_url': self._get_resource_url(activity.object), 'target_url': self._get_resource_url(activity.target), 'user_name': self._get_user_name(user), }) subject = _('mail_send_activity_subject', mapping=mapping, default='${site_name}: ${activity_name}') if activity.type == ActivityType.remove: body = _('mail_send_activity_remove_body_txt', mapping=mapping, default=u'${activity_description} Visit ' '${target_type_name}: ${target_url} .' ) else: body = _('mail_send_activity_add_update_body_txt', mapping=mapping, default=u'${activity_description} Visit ' '${object_type_name}: ${object_url} .' ) user_mail = self._get_user_email(user) self.send_mail(subject=subject, recipients=[user_mail], body=body, request=request, )
def _get_resource_url(self, resource: IResource) -> str: path = resource_path(resource) return '{0}/r{1}/'.format(self.frontend_url, path)
[docs] def send_password_change_mail(self, user=IUser, request: Request=None): """Send email with link to reset the user password.""" create_reset_url = '%s/create_password_reset/' % (self.frontend_url) mapping = {'create_reset_url': create_reset_url, 'user_name': user.name, 'site_name': self.site_name, } subject = _('mail_password_change_subject', mapping=mapping, default='${site_name}: Password changed / ' 'Passwort wurde geƤndert') body = _('mail_password_change_body_txt', mapping=mapping, default='Your password has been changed.' ) self.send_mail(subject=subject, recipients=[user.email], body=body, request=request, )
[docs] def send_new_email_activation_mail(self, user: IUser, activation_path: str, new_email: str, request: Request=None): """Send a activation mail to validate the new email of a user.""" mapping = {'activation_path': activation_path, 'frontend_url': self.frontend_url, 'user_name': user.name, 'site_name': self.site_name, } subject = _('mail_new_email_verification_subject', mapping=mapping, default='${site_name}: Email Verification / ' 'Email Aktivierung') body_txt = _('mail_new_email_verification_body_txt', mapping=mapping, default='${frontend_url}${activation_path}') self.send_mail(subject=subject, recipients=[new_email], body=body_txt, request=request, )
[docs]def includeme(config): """Add Messenger to registry.""" config.registry.messenger = Messenger(config.registry)