Changeset - c935bcaf7086
[Not reviewed]
default
0 4 0
Thomas De Schampheleire - 10 years ago 2015-08-27 22:19:39
thomas.de.schampheleire@gmail.com
email: send comment and pullrequest mails with the author's name in 'From'

When emails are sent for comments and pullrequest invitations, set the From
header to:
Author's Name (no-reply) <generic email address>

Using the name of the person that causes the email, makes the emails more
useful and interpretable for the recipient of the emails.
To avoid replies directly to the author, triggering an 'offline' email
discussion that is not visible in the Kallithea interface, a generic
'no-reply' email address is used instead of the author's email
address.
This approach is assumed to be accepted by spam filters, as several other
web services are using the same approach.

The sender used for other email types, e.g. password reset mails, is
untouched and remains the value configured in app_email_from.

The sender used for the SMTP envelope is untouched as well.

Based on code by Cedric De Herdt.
4 files changed with 108 insertions and 7 deletions:
0 comments (0 inline, 0 general)
docs/usage/email.rst
Show inline comments
 
@@ -36,15 +36,20 @@ determine the intended recipients, the a
 
specified in ``email_to`` in the configuration file are used as fallback.
 

	
 
Recipients will see these emails originating from the sender specified in the
 
``app_email_from`` setting in the configuration file. This setting can either
 
contain only an email address, like `kallithea-noreply@example.com`, or both
 
a name and an address in the following format: `Kallithea
 
<kallithea-noreply@example.com>`. The subject of these emails can
 
optionally be prefixed with the value of ``email_prefix`` in the configuration
 
file.
 
<kallithea-noreply@example.com>`. However, if the email is sent due to an
 
action of a particular user, for example when a comment is given or a pull
 
request created, the name of that user will be combined with the email address
 
specified in ``app_email_from`` to form the sender (and any name part in that
 
configuration setting disregarded).
 

	
 
The subject of these emails can optionally be prefixed with the value of
 
``email_prefix`` in the configuration file.
 

	
 

	
 
Error emails
 
------------
 

	
 
When an exception occurs in Kallithea -- and unless interactive debugging is
kallithea/lib/celerylib/tasks.py
Show inline comments
 
@@ -28,12 +28,13 @@ Original author and date, and relevant c
 

	
 
from celery.decorators import task
 

	
 
import os
 
import traceback
 
import logging
 
import rfc822
 
from os.path import join as jn
 

	
 
from time import mktime
 
from operator import itemgetter
 
from string import lower
 

	
 
@@ -42,12 +43,13 @@ from pylons import config
 
from kallithea import CELERY_ON
 
from kallithea.lib.celerylib import run_task, locked_task, dbsession, \
 
    str2bool, __get_lockkey, LockHeld, DaemonLock, get_session
 
from kallithea.lib.helpers import person
 
from kallithea.lib.rcmail.smtp_mailer import SmtpMailer
 
from kallithea.lib.utils import add_cache, action_logger
 
from kallithea.lib.vcs.utils import author_email
 
from kallithea.lib.compat import json, OrderedDict
 
from kallithea.lib.hooks import log_create_repository
 

	
 
from kallithea.model.db import Statistics, Repository, User
 

	
 

	
 
@@ -244,24 +246,31 @@ def get_commits_stats(repo_name, ts_min_
 
        log.info('LockHeld')
 
        return 'Task with key %s already running' % lockkey
 

	
 

	
 
@task(ignore_result=True)
 
@dbsession
 
def send_email(recipients, subject, body='', html_body='', headers=None):
 
def send_email(recipients, subject, body='', html_body='', headers=None, author=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: body of the mail
 
    :param html_body: html version of body
 
    :param headers: dictionary of prepopulated e-mail headers
 
    :param author: User object of the author of this mail, if known and relevant
 
    """
 
    log = get_logger(send_email)
 
    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)
 

	
 
@@ -277,13 +286,24 @@ def send_email(recipients, subject, body
 
        if not recipients:
 
            log.error("No recipients specified and no fallback available.")
 
            return False
 

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

	
 
    mail_from = email_config.get('app_email_from', 'Kallithea')
 
    # SMTP sender
 
    envelope_from = email_config.get('app_email_from', 'Kallithea')
 
    # 'From' header
 
    if author is not None:
 
        # set From header based on author 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(envelope_from)
 
        headers['From'] = '"%s" <%s>' % (
 
            rfc822.quote('%s (no-reply)' % author.full_name_or_username),
 
            envelope_addr)
 

	
 
    user = email_config.get('smtp_username')
 
    passwd = email_config.get('smtp_password')
 
    mail_server = email_config.get('smtp_server')
 
    mail_port = email_config.get('smtp_port')
 
    tls = str2bool(email_config.get('smtp_use_tls'))
 
    ssl = str2bool(email_config.get('smtp_use_ssl'))
 
@@ -303,13 +323,13 @@ def send_email(recipients, subject, body
 
    else:
 
        log.error("SMTP mail server not configured - cannot send e-mail.")
 
        log.warning(logmsg)
 
        return False
 

	
 
    try:
 
        m = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth,
 
        m = SmtpMailer(envelope_from, user, passwd, mail_server, smtp_auth,
 
                       mail_port, ssl, tls, debug=debug)
 
        m.send(recipients, subject, body, html_body, headers=headers)
 
    except:
 
        log.error('Mail sending failed')
 
        log.error(traceback.format_exc())
 
        return False
kallithea/model/notification.py
Show inline comments
 
@@ -142,13 +142,13 @@ class NotificationModel(BaseModel):
 
            email_txt_body = EmailNotificationModel()\
 
                                .get_email_tmpl(type_, 'txt', **txt_kwargs)
 
            email_html_body = EmailNotificationModel()\
 
                                .get_email_tmpl(type_, 'html', **html_kwargs)
 

	
 
            run_task(tasks.send_email, [rec.email], email_subject, email_txt_body,
 
                     email_html_body, headers)
 
                     email_html_body, headers, author=created_by_obj)
 

	
 
        return notif
 

	
 
    def delete(self, user, notification):
 
        # we don't want to remove actual notification just the assignment
 
        try:
kallithea/tests/other/test_mail.py
Show inline comments
 
import mock
 

	
 
import kallithea
 
from kallithea.tests import *
 
from kallithea.model.db import User
 

	
 
class smtplib_mock(object):
 

	
 
    @classmethod
 
    def SMTP(cls, server, port, local_hostname):
 
        return smtplib_mock()
 
@@ -86,6 +87,81 @@ class TestMail(BaseTestCase):
 
        self.assertSetEqual(smtplib_mock.lastdest, set([TEST_USER_ADMIN_EMAIL]))
 
        self.assertEqual(smtplib_mock.lastsender, envelope_from)
 
        self.assertIn('From: %s' % envelope_from, smtplib_mock.lastmsg)
 
        self.assertIn('Subject: %s' % subject, smtplib_mock.lastmsg)
 
        self.assertIn(body, smtplib_mock.lastmsg)
 
        self.assertIn(html_body, 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 = User.get_by_username(TEST_USER_REGULAR_LOGIN)
 

	
 
        config_mock = {
 
            'smtp_server': mailserver,
 
            'app_email_from': envelope_from,
 
        }
 
        with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
 
            kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body, author=author)
 

	
 
        self.assertSetEqual(smtplib_mock.lastdest, set(recipients))
 
        self.assertEqual(smtplib_mock.lastsender, envelope_from)
 
        self.assertIn('From: "Kallithea Admin (no-reply)" <%s>' % envelope_from, smtplib_mock.lastmsg)
 
        self.assertIn('Subject: %s' % subject, smtplib_mock.lastmsg)
 
        self.assertIn(body, smtplib_mock.lastmsg)
 
        self.assertIn(html_body, smtplib_mock.lastmsg)
 

	
 
    def test_send_mail_with_author_full_mail_from(self):
 
        mailserver = 'smtp.mailserver.org'
 
        recipients = ['rcpt1', 'rcpt2']
 
        envelope_addr = 'noreply@mailserver.org'
 
        envelope_from = 'Some Name <%s>' % envelope_addr
 
        subject = 'subject'
 
        body = 'body'
 
        html_body = 'html_body'
 
        author = User.get_by_username(TEST_USER_REGULAR_LOGIN)
 

	
 
        config_mock = {
 
            'smtp_server': mailserver,
 
            'app_email_from': envelope_from,
 
        }
 
        with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
 
            kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body, author=author)
 

	
 
        self.assertSetEqual(smtplib_mock.lastdest, set(recipients))
 
        self.assertEqual(smtplib_mock.lastsender, envelope_from)
 
        self.assertIn('From: "Kallithea Admin (no-reply)" <%s>' % envelope_addr, smtplib_mock.lastmsg)
 
        self.assertIn('Subject: %s' % subject, smtplib_mock.lastmsg)
 
        self.assertIn(body, smtplib_mock.lastmsg)
 
        self.assertIn(html_body, 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 = User(name='foo', lastname='(fubar) "baz"')
 
        headers = {'extra': 'yes'}
 

	
 
        config_mock = {
 
            'smtp_server': mailserver,
 
            'app_email_from': envelope_from,
 
        }
 
        with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
 
            kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body,
 
                                                     author=author, headers=headers)
 

	
 
        self.assertSetEqual(smtplib_mock.lastdest, set(recipients))
 
        self.assertEqual(smtplib_mock.lastsender, envelope_from)
 
        self.assertIn(r'From: "foo (fubar) \"baz\" (no-reply)" <%s>' % envelope_from, smtplib_mock.lastmsg)
 
        self.assertIn('Subject: %s' % subject, smtplib_mock.lastmsg)
 
        self.assertIn(body, smtplib_mock.lastmsg)
 
        self.assertIn(html_body, smtplib_mock.lastmsg)
 
        self.assertIn('Extra: yes', smtplib_mock.lastmsg)
 
        # verify that headers dict hasn't mutated by send_email
 
        self.assertDictEqual(headers, {'extra': 'yes'})
0 comments (0 inline, 0 general)