Changeset - aafca212c8e2
[Not reviewed]
default
0 9 0
Mads Kiilerich (mads) - 5 years ago 2020-12-29 22:23:01
mads@kiilerich.com
celery: move send_email task to a better home in notification model

Avoid bundling everything from many different layers in one big task library.

This is more feasible now when we don't need kallithea.CELERY_APP set at import
time.
9 files changed with 152 insertions and 155 deletions:
0 comments (0 inline, 0 general)
kallithea/controllers/admin/settings.py
Show inline comments
 
@@ -35,25 +35,25 @@ from tg import tmpl_context as c
 
from tg.i18n import ugettext as _
 
from webob.exc import HTTPFound
 

	
 
import kallithea
 
import kallithea.lib.indexers.daemon
 
from kallithea.controllers import base
 
from kallithea.lib import webutils
 
from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired
 
from kallithea.lib.utils import repo2db_mapper, set_app_settings
 
from kallithea.lib.utils2 import safe_str
 
from kallithea.lib.vcs import VCSError
 
from kallithea.lib.webutils import url
 
from kallithea.model import async_tasks, db, meta
 
from kallithea.model import db, meta, notification
 
from kallithea.model.forms import ApplicationSettingsForm, ApplicationUiSettingsForm, ApplicationVisualisationForm
 
from kallithea.model.notification import EmailNotificationModel
 
from kallithea.model.scm import ScmModel
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class SettingsController(base.BaseController):
 

	
 
    @LoginRequired(allow_default_user=True)
 
    def _before(self, *args, **kwargs):
 
@@ -292,25 +292,25 @@ class SettingsController(base.BaseContro
 
                webutils.flash(_('Please enter email address'), category='error')
 
                raise HTTPFound(location=url('admin_settings_email'))
 

	
 
            test_email_txt_body = EmailNotificationModel() \
 
                .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
 
                                'txt', body=test_body)
 
            test_email_html_body = EmailNotificationModel() \
 
                .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
 
                                'html', body=test_body)
 

	
 
            recipients = [test_email] if test_email else None
 

	
 
            async_tasks.send_email(recipients, test_email_subj,
 
            notification.send_email(recipients, test_email_subj,
 
                             test_email_txt_body, test_email_html_body)
 

	
 
            webutils.flash(_('Send email task created'), category='success')
 
            raise HTTPFound(location=url('admin_settings_email'))
 

	
 
        defaults = db.Setting.get_app_settings()
 
        defaults.update(self._get_hg_ui_settings())
 

	
 
        c.ini = kallithea.CONFIG
 

	
 
        return htmlfill.render(
 
            base.render('admin/settings/settings.html'),
kallithea/lib/celery_app.py
Show inline comments
 
@@ -12,24 +12,25 @@ scope after tg.config has been initializ
 

	
 
To make sure that the config really has been initialized, we check one of the
 
mandatory settings.
 
"""
 

	
 
import logging
 

	
 

	
 
class CeleryConfig(object):
 
    imports = [
 
        'kallithea.lib.indexers.daemon',
 
        'kallithea.model.async_tasks',
 
        'kallithea.model.notification',
 
        'kallithea.model.repo',
 
    ]
 
    task_always_eager = False
 

	
 
list_config_names = {'imports', 'accept_content'}
 

	
 

	
 
desupported = set([
 
    'broker.url',
 
    'celery.accept.content',
 
    'celery.always.eager',
 
    'celery.amqp.task.result.expires',
kallithea/model/async_tasks.py
Show inline comments
 
@@ -17,45 +17,41 @@ kallithea.model.async_tasks
 

	
 
Kallithea task modules, containing all task that suppose to be run
 
by celery daemon
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Oct 6, 2010
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
import email.message
 
import email.utils
 
import os
 
import smtplib
 
import time
 
import traceback
 
from collections import OrderedDict
 
from operator import itemgetter
 
from time import mktime
 

	
 
import celery.utils.log
 
from tg import config
 

	
 
import kallithea
 
from kallithea.lib import celerylib, conf, ext_json
 
from kallithea.lib.utils2 import asbool, ascii_bytes
 
from kallithea.lib.vcs.utils import author_email, author_name
 
from kallithea.model import db, meta
 

	
 

	
 
__all__ = ['get_commits_stats', 'send_email']
 
__all__ = ['get_commits_stats']
 

	
 

	
 
log = celery.utils.log.get_task_logger(__name__)
 

	
 

	
 
def _author_username(author):
 
    """Return the username of the user identified by the email part of the 'author' string,
 
    default to the name or email.
 
    Kind of similar to h.person() ."""
 
    email = author_email(author)
 
    if email:
 
        user = db.User.get_by_email(email)
 
@@ -210,138 +206,24 @@ def get_commits_stats(repo_name, ts_min_
 

	
 
        # execute another task if celery is enabled
 
        if len(scm_repo.revisions) > 1 and asbool(kallithea.CONFIG.get('use_celery')) and recurse_limit > 0:
 
            get_commits_stats(repo_name, ts_min_y, ts_max_y, recurse_limit - 1)
 
        elif recurse_limit <= 0:
 
            log.debug('Not recursing - limit has been reached')
 
        else:
 
            log.debug('Not recursing')
 
    except celerylib.LockHeld:
 
        log.info('Task with key %s already running', lockkey)
 

	
 

	
 
@celerylib.task
 
def send_email(recipients, subject, body='', html_body='', headers=None, from_name=None):
 
    """
 
    Sends an email with defined parameters from the .ini files.
 

	
 
    :param recipients: list of recipients, if this is None, the defined email
 
        address from field 'email_to' and all admins is used instead
 
    :param subject: subject of the mail
 
    :param body: plain text body of the mail
 
    :param html_body: html version of body
 
    :param headers: dictionary of prepopulated e-mail headers
 
    :param from_name: full name to be used as sender of this mail - often a
 
    .full_name_or_username value
 
    """
 
    assert isinstance(recipients, list), recipients
 
    if headers is None:
 
        headers = {}
 
    else:
 
        # do not modify the original headers object passed by the caller
 
        headers = headers.copy()
 

	
 
    email_config = config
 
    email_prefix = email_config.get('email_prefix', '')
 
    if email_prefix:
 
        subject = "%s %s" % (email_prefix, subject)
 

	
 
    if not recipients:
 
        # if recipients are not defined we send to email_config + all admins
 
        recipients = [u.email for u in db.User.query()
 
                      .filter(db.User.admin == True).all()]
 
        if email_config.get('email_to') is not None:
 
            recipients += email_config.get('email_to').split(',')
 

	
 
        # If there are still no recipients, there are no admins and no address
 
        # configured in email_to, so return.
 
        if not recipients:
 
            log.error("No recipients specified and no fallback available.")
 
            return
 

	
 
        log.warning("No recipients specified for '%s' - sending to admins %s", subject, ' '.join(recipients))
 

	
 
    # SMTP sender
 
    app_email_from = email_config.get('app_email_from', 'Kallithea')
 
    # 'From' header
 
    if from_name is not None:
 
        # set From header based on from_name but with a generic e-mail address
 
        # In case app_email_from is in "Some Name <e-mail>" format, we first
 
        # extract the e-mail address.
 
        envelope_addr = author_email(app_email_from)
 
        headers['From'] = '"%s" <%s>' % (
 
            email.utils.quote('%s (no-reply)' % from_name),
 
            envelope_addr)
 

	
 
    smtp_server = email_config.get('smtp_server')
 
    smtp_port = email_config.get('smtp_port')
 
    smtp_use_tls = asbool(email_config.get('smtp_use_tls'))
 
    smtp_use_ssl = asbool(email_config.get('smtp_use_ssl'))
 
    smtp_auth = email_config.get('smtp_auth')  # undocumented - overrule automatic choice of auth mechanism
 
    smtp_username = email_config.get('smtp_username')
 
    smtp_password = email_config.get('smtp_password')
 

	
 
    logmsg = ("Mail details:\n"
 
              "recipients: %s\n"
 
              "headers: %s\n"
 
              "subject: %s\n"
 
              "body:\n%s\n"
 
              "html:\n%s\n"
 
              % (' '.join(recipients), headers, subject, body, html_body))
 

	
 
    if smtp_server:
 
        log.debug("Sending e-mail. " + logmsg)
 
    else:
 
        log.error("SMTP mail server not configured - cannot send e-mail.")
 
        log.warning(logmsg)
 
        return
 

	
 
    msg = email.message.EmailMessage()
 
    msg['Subject'] = subject
 
    msg['From'] = app_email_from  # fallback - might be overridden by a header
 
    msg['To'] = ', '.join(recipients)
 
    msg['Date'] = email.utils.formatdate(time.time())
 

	
 
    for key, value in headers.items():
 
        del msg[key]  # Delete key first to make sure add_header will replace header (if any), no matter the casing
 
        msg.add_header(key, value)
 

	
 
    msg.set_content(body)
 
    msg.add_alternative(html_body, subtype='html')
 

	
 
    try:
 
        if smtp_use_ssl:
 
            smtp_serv = smtplib.SMTP_SSL(smtp_server, smtp_port)
 
        else:
 
            smtp_serv = smtplib.SMTP(smtp_server, smtp_port)
 

	
 
        if smtp_use_tls:
 
            smtp_serv.starttls()
 

	
 
        if smtp_auth:
 
            smtp_serv.ehlo()  # populate esmtp_features
 
            smtp_serv.esmtp_features["auth"] = smtp_auth
 

	
 
        if smtp_username and smtp_password is not None:
 
            smtp_serv.login(smtp_username, smtp_password)
 

	
 
        smtp_serv.sendmail(app_email_from, recipients, msg.as_string())
 
        smtp_serv.quit()
 

	
 
        log.info('Mail was sent to: %s' % recipients)
 
    except:
 
        log.error('Mail sending failed')
 
        log.error(traceback.format_exc())
 

	
 

	
 
def __get_codes_stats(repo_name):
 
    scm_repo = db.Repository.get_by_repo_name(repo_name).scm_instance
 

	
 
    tip = scm_repo.get_changeset()
 
    code_stats = {}
 

	
 
    for _topnode, _dirnodes, filenodes in tip.walk('/'):
 
        for filenode in filenodes:
 
            ext = filenode.extension.lower()
 
            if ext in conf.LANGUAGES_EXTENSIONS_MAP and not filenode.is_binary:
 
                if ext in code_stats:
 
                    code_stats[ext] += 1
kallithea/model/notification.py
Show inline comments
 
@@ -18,32 +18,39 @@ kallithea.model.notification
 
Model for notifications
 

	
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Nov 20, 2011
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
import datetime
 
import email.message
 
import email.utils
 
import logging
 
import smtplib
 
import time
 
import traceback
 

	
 
from tg import app_globals
 
from tg import app_globals, config
 
from tg import tmpl_context as c
 
from tg.i18n import ugettext as _
 

	
 
from kallithea.lib import webutils
 
from kallithea.model import async_tasks, db
 
from kallithea.lib import celerylib, webutils
 
from kallithea.lib.utils2 import asbool
 
from kallithea.lib.vcs.utils import author_email
 
from kallithea.model import db
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class NotificationModel(object):
 

	
 
    TYPE_CHANGESET_COMMENT = 'cs_comment'
 
    TYPE_MESSAGE = 'message'
 
    TYPE_MENTION = 'mention' # not used
 
    TYPE_REGISTRATION = 'registration'
 
    TYPE_PULL_REQUEST = 'pull_request'
 
@@ -124,25 +131,25 @@ class NotificationModel(object):
 

	
 
        # don't send email to the person who caused the notification, except for
 
        # notifications about new pull requests where the author is explicitly
 
        # added.
 
        rec_mails = set(obj.email for obj in recipients_objs)
 
        if type_ == NotificationModel.TYPE_PULL_REQUEST:
 
            rec_mails.add(created_by_obj.email)
 
        else:
 
            rec_mails.discard(created_by_obj.email)
 

	
 
        # send email with notification to participants
 
        for rec_mail in sorted(rec_mails):
 
            async_tasks.send_email([rec_mail], email_subject, email_txt_body,
 
            send_email([rec_mail], email_subject, email_txt_body,
 
                     email_html_body, headers,
 
                     from_name=created_by_obj.full_name_or_username)
 

	
 

	
 
class EmailNotificationModel(object):
 

	
 
    TYPE_CHANGESET_COMMENT = NotificationModel.TYPE_CHANGESET_COMMENT
 
    TYPE_MESSAGE = NotificationModel.TYPE_MESSAGE # only used for testing
 
    # NotificationModel.TYPE_MENTION is not used
 
    TYPE_PASSWORD_RESET = 'password_link'
 
    TYPE_REGISTRATION = NotificationModel.TYPE_REGISTRATION
 
    TYPE_PULL_REQUEST = NotificationModel.TYPE_PULL_REQUEST
 
@@ -219,12 +226,126 @@ class EmailNotificationModel(object):
 
                })
 
            _kwargs.update({
 
                "default_style": "%(sans_style)s;font-weight:200;font-size:14px;line-height:17px;color:%(color_text)s" % _kwargs,
 
                "comment_style": "%(monospace_style)s;white-space:pre-wrap" % _kwargs,
 
                "data_style": "border:%(color_border)s 1px solid;background:%(color_background_grey)s" % _kwargs,
 
                "emph_style": "font-weight:600;color:%(color_emph)s" % _kwargs,
 
                "link_style": "color:%(color_link)s;text-decoration:none" % _kwargs,
 
                "link_text_style": "color:%(color_text)s;text-decoration:none;border:%(color_border)s 1px solid;background:%(color_background_grey)s" % _kwargs,
 
                })
 

	
 
        log.debug('rendering tmpl %s with kwargs %s', base, _kwargs)
 
        return email_template.render_unicode(**_kwargs)
 

	
 

	
 
@celerylib.task
 
def send_email(recipients, subject, body='', html_body='', headers=None, from_name=None):
 
    """
 
    Sends an email with defined parameters from the .ini files.
 

	
 
    :param recipients: list of recipients, if this is None, the defined email
 
        address from field 'email_to' and all admins is used instead
 
    :param subject: subject of the mail
 
    :param body: plain text body of the mail
 
    :param html_body: html version of body
 
    :param headers: dictionary of prepopulated e-mail headers
 
    :param from_name: full name to be used as sender of this mail - often a
 
    .full_name_or_username value
 
    """
 
    assert isinstance(recipients, list), recipients
 
    if headers is None:
 
        headers = {}
 
    else:
 
        # do not modify the original headers object passed by the caller
 
        headers = headers.copy()
 

	
 
    email_config = config
 
    email_prefix = email_config.get('email_prefix', '')
 
    if email_prefix:
 
        subject = "%s %s" % (email_prefix, subject)
 

	
 
    if not recipients:
 
        # if recipients are not defined we send to email_config + all admins
 
        recipients = [u.email for u in db.User.query()
 
                      .filter(db.User.admin == True).all()]
 
        if email_config.get('email_to') is not None:
 
            recipients += email_config.get('email_to').split(',')
 

	
 
        # If there are still no recipients, there are no admins and no address
 
        # configured in email_to, so return.
 
        if not recipients:
 
            log.error("No recipients specified and no fallback available.")
 
            return
 

	
 
        log.warning("No recipients specified for '%s' - sending to admins %s", subject, ' '.join(recipients))
 

	
 
    # SMTP sender
 
    app_email_from = email_config.get('app_email_from', 'Kallithea')
 
    # 'From' header
 
    if from_name is not None:
 
        # set From header based on from_name but with a generic e-mail address
 
        # In case app_email_from is in "Some Name <e-mail>" format, we first
 
        # extract the e-mail address.
 
        envelope_addr = author_email(app_email_from)
 
        headers['From'] = '"%s" <%s>' % (
 
            email.utils.quote('%s (no-reply)' % from_name),
 
            envelope_addr)
 

	
 
    smtp_server = email_config.get('smtp_server')
 
    smtp_port = email_config.get('smtp_port')
 
    smtp_use_tls = asbool(email_config.get('smtp_use_tls'))
 
    smtp_use_ssl = asbool(email_config.get('smtp_use_ssl'))
 
    smtp_auth = email_config.get('smtp_auth')  # undocumented - overrule automatic choice of auth mechanism
 
    smtp_username = email_config.get('smtp_username')
 
    smtp_password = email_config.get('smtp_password')
 

	
 
    logmsg = ("Mail details:\n"
 
              "recipients: %s\n"
 
              "headers: %s\n"
 
              "subject: %s\n"
 
              "body:\n%s\n"
 
              "html:\n%s\n"
 
              % (' '.join(recipients), headers, subject, body, html_body))
 

	
 
    if smtp_server:
 
        log.debug("Sending e-mail. " + logmsg)
 
    else:
 
        log.error("SMTP mail server not configured - cannot send e-mail.")
 
        log.warning(logmsg)
 
        return
 

	
 
    msg = email.message.EmailMessage()
 
    msg['Subject'] = subject
 
    msg['From'] = app_email_from  # fallback - might be overridden by a header
 
    msg['To'] = ', '.join(recipients)
 
    msg['Date'] = email.utils.formatdate(time.time())
 

	
 
    for key, value in headers.items():
 
        del msg[key]  # Delete key first to make sure add_header will replace header (if any), no matter the casing
 
        msg.add_header(key, value)
 

	
 
    msg.set_content(body)
 
    msg.add_alternative(html_body, subtype='html')
 

	
 
    try:
 
        if smtp_use_ssl:
 
            smtp_serv = smtplib.SMTP_SSL(smtp_server, smtp_port)
 
        else:
 
            smtp_serv = smtplib.SMTP(smtp_server, smtp_port)
 

	
 
        if smtp_use_tls:
 
            smtp_serv.starttls()
 

	
 
        if smtp_auth:
 
            smtp_serv.ehlo()  # populate esmtp_features
 
            smtp_serv.esmtp_features["auth"] = smtp_auth
 

	
 
        if smtp_username and smtp_password is not None:
 
            smtp_serv.login(smtp_username, smtp_password)
 

	
 
        smtp_serv.sendmail(app_email_from, recipients, msg.as_string())
 
        smtp_serv.quit()
 

	
 
        log.info('Mail was sent to: %s' % recipients)
 
    except:
 
        log.error('Mail sending failed')
 
        log.error(traceback.format_exc())
kallithea/model/user.py
Show inline comments
 
@@ -30,25 +30,25 @@ import hashlib
 
import hmac
 
import logging
 
import time
 
import traceback
 

	
 
from sqlalchemy.exc import DatabaseError
 
from tg import config
 
from tg.i18n import ugettext as _
 

	
 
from kallithea.lib import hooks, webutils
 
from kallithea.lib.exceptions import DefaultUserException, UserOwnsReposException
 
from kallithea.lib.utils2 import check_password, generate_api_key, get_crypt_password, get_current_authuser
 
from kallithea.model import db, forms, meta
 
from kallithea.model import db, forms, meta, notification
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class UserModel(object):
 
    password_reset_token_lifetime = 86400 # 24 hours
 

	
 
    def get(self, user_id):
 
        user = db.User.query()
 
        return user.get(user_id)
 

	
 
@@ -153,26 +153,24 @@ class UserModel(object):
 
                meta.Session().add(new_user)
 
                meta.Session().flush() # make database assign new_user.user_id
 

	
 
            if not edit:
 
                hooks.log_create_user(new_user.get_dict(), cur_user)
 

	
 
            return new_user
 
        except (DatabaseError,):
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
    def create_registration(self, form_data):
 
        from kallithea.model import notification
 

	
 
        form_data['admin'] = False
 
        form_data['extern_type'] = db.User.DEFAULT_AUTH_TYPE
 
        form_data['extern_name'] = ''
 
        new_user = self.create(form_data)
 

	
 
        # notification to admins
 
        edit_url = webutils.canonical_url('edit_user', id=new_user.user_id)
 
        email_kwargs = {
 
            'registered_user_url': edit_url,
 
            'new_username': new_user.username,
 
            'new_email': new_user.email,
 
            'new_full_name': new_user.full_name}
 
@@ -289,26 +287,24 @@ class UserModel(object):
 
            msg='\0'.join([session_id, str(user.user_id), user.email, str(timestamp)]).encode('utf-8'),
 
            digestmod=hashlib.sha1,
 
        ).hexdigest()
 

	
 
    def send_reset_password_email(self, data):
 
        """
 
        Sends email with a password reset token and link to the password
 
        reset confirmation page with all information (including the token)
 
        pre-filled. Also returns URL of that page, only without the token,
 
        allowing users to copy-paste or manually enter the token from the
 
        email.
 
        """
 
        from kallithea.model import async_tasks, notification
 

	
 
        user_email = data['email']
 
        user = db.User.get_by_email(user_email)
 
        timestamp = int(time.time())
 
        if user is not None:
 
            if self.can_change_password(user):
 
                log.debug('password reset user %s found', user)
 
                token = self.get_reset_password_token(user,
 
                                                      timestamp,
 
                                                      webutils.session_csrf_secret_token())
 
                # URL must be fully qualified; but since the token is locked to
 
                # the current browser session, we must provide a URL with the
 
                # current scheme and hostname, rather than the canonical_url.
 
@@ -322,25 +318,25 @@ class UserModel(object):
 
            reg_type = notification.EmailNotificationModel.TYPE_PASSWORD_RESET
 
            body = notification.EmailNotificationModel().get_email_tmpl(
 
                reg_type, 'txt',
 
                user=user.short_contact,
 
                reset_token=token,
 
                reset_url=link)
 
            html_body = notification.EmailNotificationModel().get_email_tmpl(
 
                reg_type, 'html',
 
                user=user.short_contact,
 
                reset_token=token,
 
                reset_url=link)
 
            log.debug('sending email')
 
            async_tasks.send_email([user_email], _("Password reset link"), body, html_body)
 
            notification.send_email([user_email], _("Password reset link"), body, html_body)
 
            log.info('send new password mail to %s', user_email)
 
        else:
 
            log.debug("password reset email %s not found", user_email)
 

	
 
        return webutils.url('reset_password_confirmation',
 
                     email=user_email,
 
                     timestamp=timestamp)
 

	
 
    def verify_reset_password_token(self, email, timestamp, token):
 
        user = db.User.get_by_email(email)
 
        if user is None:
 
            log.debug("user with email %s not found", email)
 
@@ -355,36 +351,35 @@ class UserModel(object):
 
        if token_age > UserModel.password_reset_token_lifetime:
 
            log.debug('password reset token expired')
 
            return False
 

	
 
        expected_token = self.get_reset_password_token(user,
 
                                                       timestamp,
 
                                                       webutils.session_csrf_secret_token())
 
        log.debug('computed password reset token: %s', expected_token)
 
        log.debug('received password reset token: %s', token)
 
        return expected_token == token
 

	
 
    def reset_password(self, user_email, new_passwd):
 
        from kallithea.model import async_tasks
 
        user = db.User.get_by_email(user_email)
 
        if user is not None:
 
            if not self.can_change_password(user):
 
                raise Exception('trying to change password for external user')
 
            user.password = get_crypt_password(new_passwd)
 
            meta.Session().commit()
 
            log.info('change password for %s', user_email)
 
        if new_passwd is None:
 
            raise Exception('unable to set new password')
 

	
 
        async_tasks.send_email([user_email],
 
        notification.send_email([user_email],
 
                 _('Password reset notification'),
 
                 _('The password to your account %s has been changed using password reset form.') % (user.username,))
 
        log.info('send password reset mail to %s', user_email)
 

	
 
        return True
 

	
 
    def has_perm(self, user, perm):
 
        perm = db.Permission.guess_instance(perm)
 
        user = db.User.guess_instance(user)
 

	
 
        return db.UserToPerm.query().filter(db.UserToPerm.user == user) \
 
            .filter(db.UserToPerm.permission == perm).scalar() is not None
kallithea/tests/functional/test_login.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
import re
 
import time
 
import urllib.parse
 

	
 
import mock
 
from tg.util.webtest import test_context
 

	
 
import kallithea.model.async_tasks
 
import kallithea.model.notification
 
from kallithea.lib import webutils
 
from kallithea.lib.utils2 import check_password, generate_api_key
 
from kallithea.model import db, meta, validators
 
from kallithea.model.api_key import ApiKeyModel
 
from kallithea.model.user import UserModel
 
from kallithea.tests import base
 
from kallithea.tests.fixture import Fixture
 

	
 

	
 
fixture = Fixture()
 

	
 

	
 
@@ -401,25 +401,25 @@ class TestLoginController(base.TestContr
 
        new.lastname = lastname
 
        new.api_key = generate_api_key()
 
        meta.Session().add(new)
 
        meta.Session().commit()
 

	
 
        token = UserModel().get_reset_password_token(
 
            db.User.get_by_username(username), timestamp, self.session_csrf_secret_token())
 

	
 
        collected = []
 
        def mock_send_email(recipients, subject, body='', html_body='', headers=None, from_name=None):
 
            collected.append((recipients, subject, body, html_body))
 

	
 
        with mock.patch.object(kallithea.model.async_tasks, 'send_email', mock_send_email), \
 
        with mock.patch.object(kallithea.model.notification, 'send_email', mock_send_email), \
 
                mock.patch.object(time, 'time', lambda: timestamp):
 
            response = self.app.post(base.url(controller='login',
 
                                         action='password_reset'),
 
                                     {'email': email,
 
                                      '_session_csrf_secret_token': self.session_csrf_secret_token()})
 

	
 
        self.checkSessionFlash(response, 'A password reset confirmation code has been sent')
 

	
 
        ((recipients, subject, body, html_body),) = collected
 
        assert recipients == ['username@example.com']
 
        assert subject == 'Password reset link'
 
        assert '\n%s\n' % token in body
kallithea/tests/models/test_notifications.py
Show inline comments
 
import os
 
import re
 

	
 
import mock
 
from tg.util.webtest import test_context
 

	
 
import kallithea.lib.celerylib
 
import kallithea.model.async_tasks
 
from kallithea.lib import webutils
 
from kallithea.model import db, meta
 
from kallithea.model.notification import EmailNotificationModel, NotificationModel
 
from kallithea.model.user import UserModel
 
from kallithea.tests import base
 

	
 

	
 
class TestNotifications(base.TestController):
 

	
 
    def setup_method(self, method):
 
        meta.Session.remove()
 
        u1 = UserModel().create_or_update(username='u1',
 
@@ -39,50 +38,50 @@ class TestNotifications(base.TestControl
 
        self.u3 = u3.user_id
 

	
 
    def test_create_notification(self):
 
        with test_context(self.app):
 
            usrs = [self.u1, self.u2]
 

	
 
            def send_email(recipients, subject, body='', html_body='', headers=None, from_name=None):
 
                assert recipients == ['u2@example.com']
 
                assert subject == 'Test Message'
 
                assert body == "hi there"
 
                assert '>hi there<' in html_body
 
                assert from_name == 'u1 u1'
 
            with mock.patch.object(kallithea.model.async_tasks, 'send_email', send_email):
 
            with mock.patch.object(kallithea.model.notification, 'send_email', send_email):
 
                NotificationModel().create(created_by=self.u1,
 
                                                   body='hi there',
 
                                                   recipients=usrs)
 

	
 
    @mock.patch.object(webutils, 'canonical_url', (lambda arg, **kwargs: 'http://%s/?%s' % (arg, '&'.join('%s=%s' % (k, v) for (k, v) in sorted(kwargs.items())))))
 
    def test_dump_html_mails(self):
 
        # Exercise all notification types and dump them to one big html file
 
        l = []
 

	
 
        def send_email(recipients, subject, body='', html_body='', headers=None, from_name=None):
 
            l.append('<hr/>\n')
 
            l.append('<h1>%s</h1>\n' % desc) # desc is from outer scope
 
            l.append('<pre>\n')
 
            l.append('From: %s <name@example.com>\n' % from_name)
 
            l.append('To: %s\n' % ' '.join(recipients))
 
            l.append('Subject: %s\n' % subject)
 
            l.append('</pre>\n')
 
            l.append('<hr/>\n')
 
            l.append('<pre>%s</pre>\n' % body)
 
            l.append('<hr/>\n')
 
            l.append(html_body)
 
            l.append('<hr/>\n')
 

	
 
        with test_context(self.app):
 
            with mock.patch.object(kallithea.model.async_tasks, 'send_email', send_email):
 
            with mock.patch.object(kallithea.model.notification, 'send_email', send_email):
 
                pr_kwargs = dict(
 
                    pr_nice_id='#7',
 
                    pr_title='The Title',
 
                    pr_title_short='The Title',
 
                    pr_url='http://pr.org/7',
 
                    pr_target_repo='http://mainline.com/repo',
 
                    pr_target_branch='trunk',
 
                    pr_source_repo='https://dev.org/repo',
 
                    pr_source_branch='devbranch',
 
                    pr_owner=db.User.get(self.u2),
 
                    pr_owner_username='u2'
 
                    )
 
@@ -146,25 +145,25 @@ class TestNotifications(base.TestControl
 
                                new_params.append(('%s, %s=%r' % (desc, param_name, v), type_, body, kwargs))
 
                        params = new_params
 

	
 
                    for desc, type_, body, kwargs in params:
 
                        # desc is used as "global" variable
 
                        NotificationModel().create(created_by=self.u1,
 
                                                           body=body, email_kwargs=kwargs,
 
                                                           recipients=[self.u2], type_=type_)
 

	
 
                # Email type TYPE_PASSWORD_RESET has no corresponding notification type - test it directly:
 
                desc = 'TYPE_PASSWORD_RESET'
 
                kwargs = dict(user='John Doe', reset_token='decbf64715098db5b0bd23eab44bd792670ab746', reset_url='http://reset.com/decbf64715098db5b0bd23eab44bd792670ab746')
 
                kallithea.model.async_tasks.send_email(['john@doe.com'],
 
                kallithea.model.notification.send_email(['john@doe.com'],
 
                    "Password reset link",
 
                    EmailNotificationModel().get_email_tmpl(EmailNotificationModel.TYPE_PASSWORD_RESET, 'txt', **kwargs),
 
                    EmailNotificationModel().get_email_tmpl(EmailNotificationModel.TYPE_PASSWORD_RESET, 'html', **kwargs),
 
                    from_name=db.User.get(self.u1).full_name_or_username)
 

	
 
        out = '<!doctype html>\n<html lang="en">\n<head><title>Notifications</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>\n<body>\n%s\n</body>\n</html>\n' % \
 
            re.sub(r'<(/?(?:!doctype|html|head|title|meta|body)\b[^>]*)>', r'<!--\1-->', ''.join(l))
 

	
 
        outfn = os.path.join(os.path.dirname(__file__), 'test_dump_html_mails.out.html')
 
        reffn = os.path.join(os.path.dirname(__file__), 'test_dump_html_mails.ref.html')
 
        with open(outfn, 'w') as f:
 
            f.write(out)
kallithea/tests/other/test_mail.py
Show inline comments
 
@@ -16,181 +16,181 @@ class smtplib_mock(object):
 
    def ehlo(self):
 
        pass
 

	
 
    def quit(self):
 
        pass
 

	
 
    def sendmail(self, sender, dest, msg):
 
        smtplib_mock.lastsender = sender
 
        smtplib_mock.lastdest = set(dest)
 
        smtplib_mock.lastmsg = msg
 

	
 

	
 
@mock.patch('kallithea.model.async_tasks.smtplib', smtplib_mock)
 
@mock.patch('kallithea.model.notification.smtplib', smtplib_mock)
 
class TestMail(base.TestController):
 

	
 
    def test_send_mail_trivial(self):
 
        mailserver = 'smtp.mailserver.org'
 
        recipients = ['rcpt1', 'rcpt2']
 
        envelope_from = 'noreply@mailserver.org'
 
        subject = 'subject'
 
        body = 'body'
 
        html_body = 'html_body'
 

	
 
        config_mock = {
 
            'smtp_server': mailserver,
 
            'app_email_from': envelope_from,
 
        }
 
        with mock.patch('kallithea.model.async_tasks.config', config_mock):
 
            kallithea.model.async_tasks.send_email(recipients, subject, body, html_body)
 
        with mock.patch('kallithea.model.notification.config', config_mock):
 
            kallithea.model.notification.send_email(recipients, subject, body, html_body)
 

	
 
        assert smtplib_mock.lastdest == set(recipients)
 
        assert smtplib_mock.lastsender == envelope_from
 
        assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
 
        assert 'Subject: %s' % subject in smtplib_mock.lastmsg
 
        assert body in smtplib_mock.lastmsg
 
        assert html_body in smtplib_mock.lastmsg
 

	
 
    def test_send_mail_no_recipients(self):
 
        mailserver = 'smtp.mailserver.org'
 
        recipients = []
 
        envelope_from = 'noreply@mailserver.org'
 
        email_to = 'admin@mailserver.org'
 
        subject = 'subject'
 
        body = 'body'
 
        html_body = 'html_body'
 

	
 
        config_mock = {
 
            'smtp_server': mailserver,
 
            'app_email_from': envelope_from,
 
            'email_to': email_to,
 
        }
 
        with mock.patch('kallithea.model.async_tasks.config', config_mock):
 
            kallithea.model.async_tasks.send_email(recipients, subject, body, html_body)
 
        with mock.patch('kallithea.model.notification.config', config_mock):
 
            kallithea.model.notification.send_email(recipients, subject, body, html_body)
 

	
 
        assert smtplib_mock.lastdest == set([base.TEST_USER_ADMIN_EMAIL, email_to])
 
        assert smtplib_mock.lastsender == envelope_from
 
        assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
 
        assert 'Subject: %s' % subject in smtplib_mock.lastmsg
 
        assert body in smtplib_mock.lastmsg
 
        assert html_body in smtplib_mock.lastmsg
 

	
 
    def test_send_mail_no_recipients_multiple_email_to(self):
 
        mailserver = 'smtp.mailserver.org'
 
        recipients = []
 
        envelope_from = 'noreply@mailserver.org'
 
        email_to = 'admin@mailserver.org,admin2@example.com'
 
        subject = 'subject'
 
        body = 'body'
 
        html_body = 'html_body'
 

	
 
        config_mock = {
 
            'smtp_server': mailserver,
 
            'app_email_from': envelope_from,
 
            'email_to': email_to,
 
        }
 
        with mock.patch('kallithea.model.async_tasks.config', config_mock):
 
            kallithea.model.async_tasks.send_email(recipients, subject, body, html_body)
 
        with mock.patch('kallithea.model.notification.config', config_mock):
 
            kallithea.model.notification.send_email(recipients, subject, body, html_body)
 

	
 
        assert smtplib_mock.lastdest == set([base.TEST_USER_ADMIN_EMAIL] + email_to.split(','))
 
        assert smtplib_mock.lastsender == envelope_from
 
        assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
 
        assert 'Subject: %s' % subject in smtplib_mock.lastmsg
 
        assert body in smtplib_mock.lastmsg
 
        assert html_body in smtplib_mock.lastmsg
 

	
 
    def test_send_mail_no_recipients_no_email_to(self):
 
        mailserver = 'smtp.mailserver.org'
 
        recipients = []
 
        envelope_from = 'noreply@mailserver.org'
 
        subject = 'subject'
 
        body = 'body'
 
        html_body = 'html_body'
 

	
 
        config_mock = {
 
            'smtp_server': mailserver,
 
            'app_email_from': envelope_from,
 
        }
 
        with mock.patch('kallithea.model.async_tasks.config', config_mock):
 
            kallithea.model.async_tasks.send_email(recipients, subject, body, html_body)
 
        with mock.patch('kallithea.model.notification.config', config_mock):
 
            kallithea.model.notification.send_email(recipients, subject, body, html_body)
 

	
 
        assert smtplib_mock.lastdest == set([base.TEST_USER_ADMIN_EMAIL])
 
        assert smtplib_mock.lastsender == envelope_from
 
        assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
 
        assert 'Subject: %s' % subject in smtplib_mock.lastmsg
 
        assert body in smtplib_mock.lastmsg
 
        assert html_body in smtplib_mock.lastmsg
 

	
 
    def test_send_mail_with_author(self):
 
        mailserver = 'smtp.mailserver.org'
 
        recipients = ['rcpt1', 'rcpt2']
 
        envelope_from = 'noreply@mailserver.org'
 
        subject = 'subject'
 
        body = 'body'
 
        html_body = 'html_body'
 
        author = db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
 

	
 
        config_mock = {
 
            'smtp_server': mailserver,
 
            'app_email_from': envelope_from,
 
        }
 
        with mock.patch('kallithea.model.async_tasks.config', config_mock):
 
            kallithea.model.async_tasks.send_email(recipients, subject, body, html_body, from_name=author.full_name_or_username)
 
        with mock.patch('kallithea.model.notification.config', config_mock):
 
            kallithea.model.notification.send_email(recipients, subject, body, html_body, from_name=author.full_name_or_username)
 

	
 
        assert smtplib_mock.lastdest == set(recipients)
 
        assert smtplib_mock.lastsender == envelope_from
 
        assert 'From: "Kallithea Admin (no-reply)" <%s>' % envelope_from in smtplib_mock.lastmsg
 
        assert 'Subject: %s' % subject in smtplib_mock.lastmsg
 
        assert body in smtplib_mock.lastmsg
 
        assert html_body in smtplib_mock.lastmsg
 

	
 
    def test_send_mail_with_author_full_mail_from(self):
 
        mailserver = 'smtp.mailserver.org'
 
        recipients = ['ræcpt1', 'receptor2 <rcpt2@example.com>', 'tæst@example.com', 'Tæst <test@example.com>']
 
        envelope_addr = 'noreply@mailserver.org'
 
        envelope_from = 'Söme Næme <%s>' % envelope_addr
 
        subject = 'subject'
 
        body = 'body'
 
        html_body = 'html_body'
 
        author = db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
 

	
 
        config_mock = {
 
            'smtp_server': mailserver,
 
            'app_email_from': envelope_from,
 
        }
 
        with mock.patch('kallithea.model.async_tasks.config', config_mock):
 
            kallithea.model.async_tasks.send_email(recipients, subject, body, html_body, from_name=author.full_name_or_username)
 
        with mock.patch('kallithea.model.notification.config', config_mock):
 
            kallithea.model.notification.send_email(recipients, subject, body, html_body, from_name=author.full_name_or_username)
 

	
 
        assert smtplib_mock.lastdest == set(recipients)
 
        assert smtplib_mock.lastsender == envelope_from
 
        assert 'From: "Kallithea Admin (no-reply)" <%s>' % envelope_addr in smtplib_mock.lastmsg
 
        assert 'Subject: %s' % subject in smtplib_mock.lastmsg
 
        assert body in smtplib_mock.lastmsg
 
        assert html_body in smtplib_mock.lastmsg
 

	
 
    def test_send_mail_extra_headers(self):
 
        mailserver = 'smtp.mailserver.org'
 
        recipients = ['rcpt1', 'rcpt2']
 
        envelope_from = 'noreply@mailserver.org'
 
        subject = 'subject'
 
        body = 'body'
 
        html_body = 'html_body'
 
        author = db.User(name='foo', lastname='(fubar) "baz"')
 
        headers = {'extra': 'yes'}
 

	
 
        config_mock = {
 
            'smtp_server': mailserver,
 
            'app_email_from': envelope_from,
 
        }
 
        with mock.patch('kallithea.model.async_tasks.config', config_mock):
 
            kallithea.model.async_tasks.send_email(recipients, subject, body, html_body,
 
        with mock.patch('kallithea.model.notification.config', config_mock):
 
            kallithea.model.notification.send_email(recipients, subject, body, html_body,
 
                                                     from_name=author.full_name_or_username, headers=headers)
 

	
 
        assert smtplib_mock.lastdest == set(recipients)
 
        assert smtplib_mock.lastsender == envelope_from
 
        assert r'From: "foo (fubar) \"baz\" (no-reply)" <%s>' % envelope_from in smtplib_mock.lastmsg
 
        assert 'Subject: %s' % subject in smtplib_mock.lastmsg
 
        assert body in smtplib_mock.lastmsg
 
        assert html_body in smtplib_mock.lastmsg
 
        assert 'extra: yes' in smtplib_mock.lastmsg
 
        # verify that headers dict hasn't mutated by send_email
 
        assert headers == {'extra': 'yes'}
scripts/deps.py
Show inline comments
 
@@ -149,25 +149,24 @@ kallithea.model.async_tasks
 
kallithea.model.scm
 
kallithea.templates.py
 
'''.split())
 

	
 
shown_modules = normal_modules | top_modules
 

	
 
# break the chains somehow - this is a cleanup TODO list
 
known_violations = set([
 
('kallithea.lib.auth_modules', 'kallithea.lib.auth'),  # needs base&facade
 
('kallithea.lib.utils', 'kallithea.model'),  # clean up utils
 
('kallithea.lib.utils', 'kallithea.model.db'),
 
('kallithea.lib.utils', 'kallithea.model.scm'),
 
('kallithea.model.async_tasks', 'kallithea.model'),
 
('kallithea.model', 'kallithea.lib.auth'),  # auth.HasXXX
 
('kallithea.model', 'kallithea.lib.auth_modules'),  # validators
 
('kallithea.model', 'kallithea.lib.hooks'),  # clean up hooks
 
('kallithea.model', 'kallithea.model.scm'),
 
('kallithea.model.scm', 'kallithea.lib.hooks'),
 
])
 

	
 
extra_edges = [
 
('kallithea.config', 'kallithea.controllers'),  # through TG
 
('kallithea.lib.auth', 'kallithea.lib.auth_modules'),  # custom loader
 
]
 

	
0 comments (0 inline, 0 general)