Commits

Marcin Kuzminski  committed 7ff304d

Notification fixes
- email prefix added to .ini files
- html templates emails
- rewrote email system to use some parts from pyramid_mailer

  • Participants
  • Parent commits 7d1fc25

Comments (0)

Files changed (22)

File development.ini

 #error_email_from = paste_error@localhost
 #app_email_from = rhodecode-noreply@localhost
 #error_message =
+#email_prefix = [RhodeCode]
 
 #smtp_server = mail.server.com
 #smtp_username = 

File production.ini

 #error_email_from = paste_error@localhost
 #app_email_from = rhodecode-noreply@localhost
 #error_message =
+#email_prefix = [RhodeCode]
 
 #smtp_server = mail.server.com
 #smtp_username = 

File rhodecode/config/deployment.ini_tmpl

 #error_email_from = paste_error@localhost
 #app_email_from = rhodecode-noreply@localhost
 #error_message =
+#email_prefix = [RhodeCode]
 
 #smtp_server = mail.server.com
 #smtp_username = 

File rhodecode/controllers/admin/settings.py

 from rhodecode.model.scm import ScmModel
 from rhodecode.model.user import UserModel
 from rhodecode.model.db import User
-from rhodecode.model.notification import NotificationModel
+from rhodecode.model.notification import NotificationModel, \
+    EmailNotificationModel
 
 log = logging.getLogger(__name__)
 
             test_email = request.POST.get('test_email')
             test_email_subj = 'RhodeCode TestEmail'
             test_email_body = 'RhodeCode Email test'
+            test_email_html_body = EmailNotificationModel()\
+                .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT)
 
             run_task(tasks.send_email, [test_email], test_email_subj,
-                     test_email_body)
+                     test_email_body, test_email_html_body)
+
             h.flash(_('Email task created'), category='success')
         return redirect(url('admin_settings'))
 

File rhodecode/lib/celerylib/tasks.py

 import os
 import traceback
 import logging
-from os.path import dirname as dn, join as jn
+from os.path import join as jn
 
 from time import mktime
 from operator import itemgetter
 from pylons import config, url
 from pylons.i18n.translation import _
 
+
 from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, safe_str
 from rhodecode.lib.celerylib import run_task, locked_task, str2bool, \
     __get_lockkey, LockHeld, DaemonLock
 from rhodecode.lib.helpers import person
-from rhodecode.lib.smtp_mailer import SmtpMailer
+from rhodecode.lib.rcmail.smtp_mailer import SmtpMailer
 from rhodecode.lib.utils import add_cache
 from rhodecode.lib.compat import json, OrderedDict
 
 
 from sqlalchemy import engine_from_config
 
+
 add_cache(config)
 
 __all__ = ['whoosh_index', 'get_commits_stats',
     sa = meta.Session()
     return sa
 
+def get_logger(cls):
+    if CELERY_ON:
+        try:
+            log = cls.get_logger()
+        except:
+            log = logging.getLogger(__name__)
+    else:
+        log = logging.getLogger(__name__)
+
+    return log
 
 def get_repos_path():
     sa = get_session()
 
 @task(ignore_result=True)
 def get_commits_stats(repo_name, ts_min_y, ts_max_y):
-    try:
-        log = get_commits_stats.get_logger()
-    except:
-        log = logging.getLogger(__name__)
+    log = get_logger(get_commits_stats)
 
     lockkey = __get_lockkey('get_commits_stats', repo_name, ts_min_y,
                             ts_max_y)
 
 @task(ignore_result=True)
 def send_password_link(user_email):
-    try:
-        log = reset_user_password.get_logger()
-    except:
-        log = logging.getLogger(__name__)
-
-    from rhodecode.lib import auth
-    from rhodecode.model.db import User
+    log = get_logger(send_password_link)
 
     try:
+        from rhodecode.model.notification import EmailNotificationModel
         sa = get_session()
-        user = sa.query(User).filter(User.email == user_email).scalar()
-
+        user = User.get_by_email(user_email)
         if user:
+            log.debug('password reset user found %s' % user)
             link = url('reset_password_confirmation', key=user.api_key,
                        qualified=True)
-            tmpl = """
-Hello %s
-
-We received a request to create a new password for your account.
-
-You can generate it by clicking following URL:
-
-%s
-
-If you didn't request new password please ignore this email.
-            """
+            reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET
+            body = EmailNotificationModel().get_email_tmpl(reg_type,
+                                                **{'user':user.short_contact,
+                                                   'reset_url':link})
+            log.debug('sending email')
             run_task(send_email, user_email,
-                     "RhodeCode password reset link",
-                     tmpl % (user.short_contact, link))
+                     _("password reset link"), body)
             log.info('send new password mail to %s', user_email)
-
+        else:
+            log.debug("password reset email %s not found" % user_email)
     except:
-        log.error('Failed to update user password')
         log.error(traceback.format_exc())
         return False
 
 
 @task(ignore_result=True)
 def reset_user_password(user_email):
-    try:
-        log = reset_user_password.get_logger()
-    except:
-        log = logging.getLogger(__name__)
+    log = get_logger(reset_user_password)
 
     from rhodecode.lib import auth
-    from rhodecode.model.db import User
 
     try:
         try:
             sa = get_session()
-            user = sa.query(User).filter(User.email == user_email).scalar()
+            user = User.get_by_email(user_email)
             new_passwd = auth.PasswordGenerator().gen_password(8,
                              auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
             if user:
                 log.info('change password for %s', user_email)
             if new_passwd is None:
                 raise Exception('unable to generate new password')
-
         except:
             log.error(traceback.format_exc())
             sa.rollback()
 
         run_task(send_email, user_email,
-                 "Your new RhodeCode password",
+                 'Your new password',
                  'Your new RhodeCode password:%s' % (new_passwd))
         log.info('send new password mail to %s', user_email)
 
 
 
 @task(ignore_result=True)
-def send_email(recipients, subject, body):
+def send_email(recipients, subject, body, html_body=''):
     """
     Sends an email with defined parameters from the .ini files.
 
         address from field 'email_to' is used instead
     :param subject: subject of the mail
     :param body: body of the mail
+    :param html_body: html version of body
     """
-    try:
-        log = send_email.get_logger()
-    except:
-        log = logging.getLogger(__name__)
-
+    log = get_logger(send_email)
     email_config = config
 
+    subject = "%s %s" % (email_config.get('email_prefix'), subject)
     if not recipients:
         # if recipients are not defined we send to email_config + all admins
-        admins = [u.email for u in User.query().filter(User.admin == True).all()]
+        admins = [u.email for u in User.query()
+                  .filter(User.admin == True).all()]
         recipients = [email_config.get('email_to')] + admins
 
-    mail_from = email_config.get('app_email_from')
+    mail_from = email_config.get('app_email_from', 'RhodeCode')
     user = email_config.get('smtp_username')
     passwd = email_config.get('smtp_password')
     mail_server = email_config.get('smtp_server')
     try:
         m = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth,
                        mail_port, ssl, tls, debug=debug)
-        m.send(recipients, subject, body)
+        m.send(recipients, subject, body, html_body)
     except:
         log.error('Mail sending failed')
         log.error(traceback.format_exc())
 
 @task(ignore_result=True)
 def create_repo_fork(form_data, cur_user):
+    log = get_logger(create_repo_fork)
+
     from rhodecode.model.repo import RepoModel
     from vcs import get_backend
 
-    try:
-        log = create_repo_fork.get_logger()
-    except:
-        log = logging.getLogger(__name__)
-
     repo_model = RepoModel(get_session())
     repo_model.create(form_data, cur_user, just_db=True, fork=True)
     repo_name = form_data['repo_name']

File rhodecode/lib/rcmail/__init__.py

Empty file added.

File rhodecode/lib/rcmail/exceptions.py

+
+class InvalidMessage(RuntimeError):
+    """
+    Raised if message is missing vital headers, such
+    as recipients or sender address.
+    """
+
+class BadHeaders(RuntimeError): 
+    """
+    Raised if message contains newlines in headers.
+    """

File rhodecode/lib/rcmail/message.py

+from rhodecode.lib.rcmail.response import MailResponse
+
+from rhodecode.lib.rcmail.exceptions import BadHeaders
+from rhodecode.lib.rcmail.exceptions import InvalidMessage
+
+class Attachment(object):
+    """
+    Encapsulates file attachment information.
+
+    :param filename: filename of attachment
+    :param content_type: file mimetype
+    :param data: the raw file data, either as string or file obj
+    :param disposition: content-disposition (if any)
+    """
+
+    def __init__(self, 
+                 filename=None, 
+                 content_type=None, 
+                 data=None,
+                 disposition=None): 
+
+        self.filename = filename
+        self.content_type = content_type
+        self.disposition = disposition or 'attachment'
+        self._data = data
+
+    @property
+    def data(self):
+        if isinstance(self._data, basestring):
+            return self._data
+        self._data = self._data.read()
+        return self._data
+
+
+class Message(object):
+    """
+    Encapsulates an email message.
+
+    :param subject: email subject header
+    :param recipients: list of email addresses
+    :param body: plain text message
+    :param html: HTML message
+    :param sender: email sender address
+    :param cc: CC list
+    :param bcc: BCC list
+    :param extra_headers: dict of extra email headers
+    :param attachments: list of Attachment instances
+    """
+
+    def __init__(self, 
+                 subject=None, 
+                 recipients=None, 
+                 body=None, 
+                 html=None, 
+                 sender=None,
+                 cc=None,
+                 bcc=None,
+                 extra_headers=None,
+                 attachments=None):
+
+
+        self.subject = subject or ''
+        self.sender = sender
+        self.body = body
+        self.html = html
+
+        self.recipients = recipients or []
+        self.attachments = attachments or []
+        self.cc = cc or []
+        self.bcc = bcc or []
+        self.extra_headers = extra_headers or {}
+
+    @property
+    def send_to(self):
+        return set(self.recipients) | set(self.bcc or ()) | set(self.cc or ())
+
+    def to_message(self):
+        """
+        Returns raw email.Message instance.Validates message first.
+        """
+        
+        self.validate()
+
+        return self.get_response().to_message()
+
+    def get_response(self):
+        """
+        Creates a Lamson MailResponse instance
+        """
+
+        response = MailResponse(Subject=self.subject, 
+                                To=self.recipients,
+                                From=self.sender,
+                                Body=self.body,
+                                Html=self.html)
+
+        if self.bcc:
+            response.base['Bcc'] = self.bcc
+
+        if self.cc:
+            response.base['Cc'] = self.cc
+
+        for attachment in self.attachments:
+
+            response.attach(attachment.filename, 
+                            attachment.content_type, 
+                            attachment.data, 
+                            attachment.disposition)
+
+        response.update(self.extra_headers)
+
+        return response
+    
+    def is_bad_headers(self):
+        """
+        Checks for bad headers i.e. newlines in subject, sender or recipients.
+        """
+       
+        headers = [self.subject, self.sender]
+        headers += list(self.send_to)
+        headers += self.extra_headers.values()
+
+        for val in headers:
+            for c in '\r\n':
+                if c in val:
+                    return True
+        return False
+        
+    def validate(self):
+        """
+        Checks if message is valid and raises appropriate exception.
+        """
+
+        if not self.recipients:
+            raise InvalidMessage, "No recipients have been added"
+
+        if not self.body and not self.html:
+            raise InvalidMessage, "No body has been set"
+
+        if not self.sender:
+            raise InvalidMessage, "No sender address has been set"
+
+        if self.is_bad_headers():
+            raise BadHeaders
+
+    def add_recipient(self, recipient):
+        """
+        Adds another recipient to the message.
+        
+        :param recipient: email address of recipient.
+        """
+        
+        self.recipients.append(recipient)
+
+    def add_cc(self, recipient):
+        """
+        Adds an email address to the CC list. 
+
+        :param recipient: email address of recipient.
+        """
+
+        self.cc.append(recipient)
+
+    def add_bcc(self, recipient):
+        """
+        Adds an email address to the BCC list. 
+
+        :param recipient: email address of recipient.
+        """
+
+        self.bcc.append(recipient)
+
+    def attach(self, attachment):
+        """
+        Adds an attachment to the message.
+
+        :param attachment: an **Attachment** instance.
+        """
+
+        self.attachments.append(attachment)
+
+

File rhodecode/lib/rcmail/response.py

+# The code in this module is entirely lifted from the Lamson project
+# (http://lamsonproject.org/).  Its copyright is:
+
+# Copyright (c) 2008, Zed A. Shaw
+# All rights reserved.
+
+# It is provided under this license:
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+
+# * Neither the name of the Zed A. Shaw nor the names of its contributors may
+#   be used to endorse or promote products derived from this software without
+#   specific prior written permission.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import mimetypes
+import string
+from email import encoders
+from email.charset import Charset
+from email.utils import parseaddr
+from email.mime.base import MIMEBase
+
+ADDRESS_HEADERS_WHITELIST = ['From', 'To', 'Delivered-To', 'Cc', 'Bcc']
+DEFAULT_ENCODING = "utf-8"
+VALUE_IS_EMAIL_ADDRESS = lambda v: '@' in v
+
+def normalize_header(header):
+    return string.capwords(header.lower(), '-')
+
+class EncodingError(Exception): 
+    """Thrown when there is an encoding error."""
+    pass
+
+class MailBase(object):
+    """MailBase is used as the basis of lamson.mail and contains the basics of
+    encoding an email.  You actually can do all your email processing with this
+    class, but it's more raw.
+    """
+    def __init__(self, items=()):
+        self.headers = dict(items)
+        self.parts = []
+        self.body = None
+        self.content_encoding = {'Content-Type': (None, {}), 
+                                 'Content-Disposition': (None, {}),
+                                 'Content-Transfer-Encoding': (None, {})}
+
+    def __getitem__(self, key):
+        return self.headers.get(normalize_header(key), None)
+
+    def __len__(self):
+        return len(self.headers)
+
+    def __iter__(self):
+        return iter(self.headers)
+
+    def __contains__(self, key):
+        return normalize_header(key) in self.headers
+
+    def __setitem__(self, key, value):
+        self.headers[normalize_header(key)] = value
+
+    def __delitem__(self, key):
+        del self.headers[normalize_header(key)]
+
+    def __nonzero__(self):
+        return self.body != None or len(self.headers) > 0 or len(self.parts) > 0
+
+    def keys(self):
+        """Returns the sorted keys."""
+        return sorted(self.headers.keys())
+
+    def attach_file(self, filename, data, ctype, disposition):
+        """
+        A file attachment is a raw attachment with a disposition that
+        indicates the file name.
+        """
+        assert filename, "You can't attach a file without a filename."
+        ctype = ctype.lower()
+
+        part = MailBase()
+        part.body = data
+        part.content_encoding['Content-Type'] = (ctype, {'name': filename})
+        part.content_encoding['Content-Disposition'] = (disposition,
+                                                        {'filename': filename})
+        self.parts.append(part)
+
+
+    def attach_text(self, data, ctype):
+        """
+        This attaches a simpler text encoded part, which doesn't have a
+        filename.
+        """
+        ctype = ctype.lower()
+
+        part = MailBase()
+        part.body = data
+        part.content_encoding['Content-Type'] = (ctype, {})
+        self.parts.append(part)
+
+    def walk(self):
+        for p in self.parts:
+            yield p
+            for x in p.walk():
+                yield x
+
+class MailResponse(object):
+    """
+    You are given MailResponse objects from the lamson.view methods, and
+    whenever you want to generate an email to send to someone.  It has the
+    same basic functionality as MailRequest, but it is designed to be written
+    to, rather than read from (although you can do both).
+
+    You can easily set a Body or Html during creation or after by passing it
+    as __init__ parameters, or by setting those attributes.
+
+    You can initially set the From, To, and Subject, but they are headers so
+    use the dict notation to change them: msg['From'] = 'joe@test.com'.
+
+    The message is not fully crafted until right when you convert it with
+    MailResponse.to_message.  This lets you change it and work with it, then
+    send it out when it's ready.
+    """
+    def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None):
+        self.Body = Body
+        self.Html = Html
+        self.base = MailBase([('To', To), ('From', From), ('Subject', Subject)])
+        self.multipart = self.Body and self.Html
+        self.attachments = []
+
+    def __contains__(self, key):
+        return self.base.__contains__(key)
+
+    def __getitem__(self, key):
+        return self.base.__getitem__(key)
+
+    def __setitem__(self, key, val):
+        return self.base.__setitem__(key, val)
+
+    def __delitem__(self, name):
+        del self.base[name]
+
+    def attach(self, filename=None, content_type=None, data=None,
+               disposition=None):
+        """
+
+        Simplifies attaching files from disk or data as files.  To attach
+        simple text simple give data and a content_type.  To attach a file,
+        give the data/content_type/filename/disposition combination.
+
+        For convenience, if you don't give data and only a filename, then it
+        will read that file's contents when you call to_message() later.  If
+        you give data and filename then it will assume you've filled data
+        with what the file's contents are and filename is just the name to
+        use.
+        """
+
+        assert filename or data, ("You must give a filename or some data to "
+                                  "attach.")
+        assert data or os.path.exists(filename), ("File doesn't exist, and no "
+                                                  "data given.")
+
+        self.multipart = True
+
+        if filename and not content_type:
+            content_type, encoding = mimetypes.guess_type(filename)
+
+        assert content_type, ("No content type given, and couldn't guess "
+                              "from the filename: %r" % filename)
+
+        self.attachments.append({'filename': filename,
+                                 'content_type': content_type,
+                                 'data': data,
+                                 'disposition': disposition,})
+    def attach_part(self, part):
+        """
+        Attaches a raw MailBase part from a MailRequest (or anywhere)
+        so that you can copy it over.
+        """
+        self.multipart = True
+
+        self.attachments.append({'filename': None,
+                                 'content_type': None,
+                                 'data': None,
+                                 'disposition': None,
+                                 'part': part,
+                                 })
+
+    def attach_all_parts(self, mail_request):
+        """
+        Used for copying the attachment parts of a mail.MailRequest
+        object for mailing lists that need to maintain attachments.
+        """
+        for part in mail_request.all_parts():
+            self.attach_part(part)
+
+        self.base.content_encoding = mail_request.base.content_encoding.copy()
+
+    def clear(self):
+        """
+        Clears out the attachments so you can redo them.  Use this to keep the
+        headers for a series of different messages with different attachments.
+        """
+        del self.attachments[:]
+        del self.base.parts[:]
+        self.multipart = False
+
+
+    def update(self, message):
+        """
+        Used to easily set a bunch of heading from another dict
+        like object.
+        """
+        for k in message.keys():
+            self.base[k] = message[k]
+
+    def __str__(self):
+        """
+        Converts to a string.
+        """
+        return self.to_message().as_string()
+
+    def _encode_attachment(self, filename=None, content_type=None, data=None,
+                           disposition=None, part=None):
+        """
+        Used internally to take the attachments mentioned in self.attachments
+        and do the actual encoding in a lazy way when you call to_message.
+        """
+        if part:
+            self.base.parts.append(part)
+        elif filename:
+            if not data:
+                data = open(filename).read()
+
+            self.base.attach_file(filename, data, content_type,
+                                  disposition or 'attachment')
+        else:
+            self.base.attach_text(data, content_type)
+
+        ctype = self.base.content_encoding['Content-Type'][0]
+
+        if ctype and not ctype.startswith('multipart'):
+            self.base.content_encoding['Content-Type'] = ('multipart/mixed', {})
+
+    def to_message(self):
+        """
+        Figures out all the required steps to finally craft the
+        message you need and return it.  The resulting message
+        is also available as a self.base attribute.
+
+        What is returned is a Python email API message you can
+        use with those APIs.  The self.base attribute is the raw
+        lamson.encoding.MailBase.
+        """
+        del self.base.parts[:]
+
+        if self.Body and self.Html:
+            self.multipart = True
+            self.base.content_encoding['Content-Type'] = (
+                'multipart/alternative', {})
+
+        if self.multipart:
+            self.base.body = None
+            if self.Body:
+                self.base.attach_text(self.Body, 'text/plain')
+
+            if self.Html:
+                self.base.attach_text(self.Html, 'text/html')
+
+            for args in self.attachments:
+                self._encode_attachment(**args)
+
+        elif self.Body:
+            self.base.body = self.Body
+            self.base.content_encoding['Content-Type'] = ('text/plain', {})
+
+        elif self.Html:
+            self.base.body = self.Html
+            self.base.content_encoding['Content-Type'] = ('text/html', {})
+
+        return to_message(self.base)
+
+    def all_parts(self):
+        """
+        Returns all the encoded parts.  Only useful for debugging
+        or inspecting after calling to_message().
+        """
+        return self.base.parts
+
+    def keys(self):
+        return self.base.keys()
+
+def to_message(mail):
+    """
+    Given a MailBase message, this will construct a MIMEPart 
+    that is canonicalized for use with the Python email API.
+    """
+    ctype, params = mail.content_encoding['Content-Type']
+
+    if not ctype:
+        if mail.parts:
+            ctype = 'multipart/mixed'
+        else:
+            ctype = 'text/plain'
+    else:
+        if mail.parts:
+            assert ctype.startswith(("multipart", "message")), \
+                   "Content type should be multipart or message, not %r" % ctype
+
+    # adjust the content type according to what it should be now
+    mail.content_encoding['Content-Type'] = (ctype, params)
+
+    try:
+        out = MIMEPart(ctype, **params)
+    except TypeError, exc: # pragma: no cover
+        raise EncodingError("Content-Type malformed, not allowed: %r; "
+                            "%r (Python ERROR: %s" %
+                            (ctype, params, exc.message))
+
+    for k in mail.keys():
+        if k in ADDRESS_HEADERS_WHITELIST:
+            out[k.encode('ascii')] = header_to_mime_encoding(mail[k])
+        else:
+            out[k.encode('ascii')] = header_to_mime_encoding(mail[k],
+                                                             not_email=True)
+
+    out.extract_payload(mail)
+
+    # go through the children
+    for part in mail.parts:
+        out.attach(to_message(part))
+
+    return out
+
+class MIMEPart(MIMEBase):
+    """
+    A reimplementation of nearly everything in email.mime to be more useful
+    for actually attaching things.  Rather than one class for every type of
+    thing you'd encode, there's just this one, and it figures out how to
+    encode what you ask it.
+    """
+    def __init__(self, type, **params):
+        self.maintype, self.subtype = type.split('/')
+        MIMEBase.__init__(self, self.maintype, self.subtype, **params)
+
+    def add_text(self, content):
+        # this is text, so encode it in canonical form
+        try:
+            encoded = content.encode('ascii')
+            charset = 'ascii'
+        except UnicodeError:
+            encoded = content.encode('utf-8')
+            charset = 'utf-8'
+
+        self.set_payload(encoded, charset=charset)
+
+
+    def extract_payload(self, mail):
+        if mail.body == None: return  # only None, '' is still ok
+
+        ctype, ctype_params = mail.content_encoding['Content-Type']
+        cdisp, cdisp_params = mail.content_encoding['Content-Disposition']
+
+        assert ctype, ("Extract payload requires that mail.content_encoding "
+                       "have a valid Content-Type.")
+
+        if ctype.startswith("text/"):
+            self.add_text(mail.body)
+        else:
+            if cdisp:
+                # replicate the content-disposition settings
+                self.add_header('Content-Disposition', cdisp, **cdisp_params)
+
+            self.set_payload(mail.body)
+            encoders.encode_base64(self)
+
+    def __repr__(self):
+        return "<MIMEPart '%s/%s': %r, %r, multipart=%r>" % (
+            self.subtype,
+            self.maintype,
+            self['Content-Type'],
+            self['Content-Disposition'],
+            self.is_multipart())
+
+
+def header_to_mime_encoding(value, not_email=False):
+    if not value: return ""
+
+    encoder = Charset(DEFAULT_ENCODING)
+    if type(value) == list:
+        return "; ".join(properly_encode_header(
+            v, encoder, not_email) for v in value)
+    else:
+        return properly_encode_header(value, encoder, not_email)
+
+def properly_encode_header(value, encoder, not_email):
+    """
+    The only thing special (weird) about this function is that it tries
+    to do a fast check to see if the header value has an email address in
+    it.  Since random headers could have an email address, and email addresses
+    have weird special formatting rules, we have to check for it.
+
+    Normally this works fine, but in Librelist, we need to "obfuscate" email
+    addresses by changing the '@' to '-AT-'.  This is where
+    VALUE_IS_EMAIL_ADDRESS exists.  It's a simple lambda returning True/False
+    to check if a header value has an email address.  If you need to make this
+    check different, then change this.
+    """
+    try:
+        return value.encode("ascii")
+    except UnicodeEncodeError:
+        if not_email is False and VALUE_IS_EMAIL_ADDRESS(value):
+            # this could have an email address, make sure we don't screw it up
+            name, address = parseaddr(value)
+            return '"%s" <%s>' % (
+                encoder.header_encode(name.encode("utf-8")), address)
+
+        return encoder.header_encode(value.encode("utf-8"))

File rhodecode/lib/rcmail/smtp_mailer.py

+# -*- coding: utf-8 -*-
+"""
+    rhodecode.lib.rcmail.smtp_mailer
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Simple smtp mailer used in RhodeCode
+
+    :created_on: Sep 13, 2010
+    :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
+    :license: GPLv3, see COPYING for more details.
+"""
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+import smtplib
+from socket import sslerror
+from rhodecode.lib.rcmail.message import Message
+
+class SmtpMailer(object):
+    """SMTP mailer class
+
+    mailer = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth
+                        mail_port, ssl, tls)
+    mailer.send(recipients, subject, body, attachment_files)
+
+    :param recipients might be a list of string or single string
+    :param attachment_files is a dict of {filename:location}
+        it tries to guess the mimetype and attach the file
+
+    """
+
+    def __init__(self, mail_from, user, passwd, mail_server, smtp_auth=None,
+                 mail_port=None, ssl=False, tls=False, debug=False):
+
+        self.mail_from = mail_from
+        self.mail_server = mail_server
+        self.mail_port = mail_port
+        self.user = user
+        self.passwd = passwd
+        self.ssl = ssl
+        self.tls = tls
+        self.debug = debug
+        self.auth = smtp_auth
+
+
+    def send(self, recipients=[], subject='', body='', html='',
+             attachment_files=None):
+
+        if isinstance(recipients, basestring):
+            recipients = [recipients]
+        msg = Message(subject, recipients, body, html, self.mail_from)
+        raw_msg = msg.to_message()
+
+        if self.ssl:
+            smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port)
+        else:
+            smtp_serv = smtplib.SMTP(self.mail_server, self.mail_port)
+
+        if self.tls:
+            smtp_serv.ehlo()
+            smtp_serv.starttls()
+
+        if self.debug:
+            smtp_serv.set_debuglevel(1)
+
+        smtp_serv.ehlo()
+        if self.auth:
+            smtp_serv.esmtp_features["auth"] = self.auth
+
+        # if server requires authorization you must provide login and password
+        # but only if we have them
+        if self.user and self.passwd:
+            smtp_serv.login(self.user, self.passwd)
+
+        smtp_serv.sendmail(msg.sender, msg.send_to, raw_msg.as_string())
+        logging.info('MAIL SEND TO: %s' % recipients)
+
+        try:
+            smtp_serv.quit()
+        except sslerror:
+            # sslerror is raised in tls connections on closing sometimes
+            pass

File rhodecode/lib/smtp_mailer.py

-# -*- coding: utf-8 -*-
-"""
-    rhodecode.lib.smtp_mailer
-    ~~~~~~~~~~~~~~~~~~~~~~~~~
-
-    Simple smtp mailer used in RhodeCode
-
-    :created_on: Sep 13, 2010
-    :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
-    :license: GPLv3, see COPYING for more details.
-"""
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import logging
-import smtplib
-import mimetypes
-from socket import sslerror
-
-from email.mime.multipart import MIMEMultipart
-from email.mime.image import MIMEImage
-from email.mime.audio import MIMEAudio
-from email.mime.base import MIMEBase
-from email.mime.text import MIMEText
-from email.utils import formatdate
-from email import encoders
-
-
-class SmtpMailer(object):
-    """SMTP mailer class
-
-    mailer = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth
-                        mail_port, ssl, tls)
-    mailer.send(recipients, subject, body, attachment_files)
-
-    :param recipients might be a list of string or single string
-    :param attachment_files is a dict of {filename:location}
-        it tries to guess the mimetype and attach the file
-
-    """
-
-    def __init__(self, mail_from, user, passwd, mail_server, smtp_auth=None,
-                 mail_port=None, ssl=False, tls=False, debug=False):
-
-        self.mail_from = mail_from
-        self.mail_server = mail_server
-        self.mail_port = mail_port
-        self.user = user
-        self.passwd = passwd
-        self.ssl = ssl
-        self.tls = tls
-        self.debug = debug
-        self.auth = smtp_auth
-
-    def send(self, recipients=[], subject='', body='', attachment_files=None):
-
-        if isinstance(recipients, basestring):
-            recipients = [recipients]
-        if self.ssl:
-            smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port)
-        else:
-            smtp_serv = smtplib.SMTP(self.mail_server, self.mail_port)
-
-        if self.tls:
-            smtp_serv.ehlo()
-            smtp_serv.starttls()
-
-        if self.debug:
-            smtp_serv.set_debuglevel(1)
-
-        smtp_serv.ehlo()
-        if self.auth:
-            smtp_serv.esmtp_features["auth"] = self.auth
-
-        # if server requires authorization you must provide login and password
-        # but only if we have them
-        if self.user and self.passwd:
-            smtp_serv.login(self.user, self.passwd)
-
-        date_ = formatdate(localtime=True)
-        msg = MIMEMultipart()
-        msg.set_type('multipart/alternative')
-        msg.preamble = 'You will not see this in a MIME-aware mail reader.\n'
-
-        text_msg = MIMEText(body)
-        text_msg.set_type('text/plain')
-        text_msg.set_param('charset', 'UTF-8')
-
-        msg['From'] = self.mail_from
-        msg['To'] = ','.join(recipients)
-        msg['Date'] = date_
-        msg['Subject'] = subject
-
-        msg.attach(text_msg)
-
-        if attachment_files:
-            self.__atach_files(msg, attachment_files)
-
-        smtp_serv.sendmail(self.mail_from, recipients, msg.as_string())
-        logging.info('MAIL SEND TO: %s' % recipients)
-
-        try:
-            smtp_serv.quit()
-        except sslerror:
-            # sslerror is raised in tls connections on closing sometimes
-            pass
-
-    def __atach_files(self, msg, attachment_files):
-        if isinstance(attachment_files, dict):
-            for f_name, msg_file in attachment_files.items():
-                ctype, encoding = mimetypes.guess_type(f_name)
-                logging.info("guessing file %s type based on %s", ctype,
-                             f_name)
-                if ctype is None or encoding is not None:
-                    # No guess could be made, or the file is encoded
-                    # (compressed), so use a generic bag-of-bits type.
-                    ctype = 'application/octet-stream'
-                maintype, subtype = ctype.split('/', 1)
-                if maintype == 'text':
-                    # Note: we should handle calculating the charset
-                    file_part = MIMEText(self.get_content(msg_file),
-                                         _subtype=subtype)
-                elif maintype == 'image':
-                    file_part = MIMEImage(self.get_content(msg_file),
-                                          _subtype=subtype)
-                elif maintype == 'audio':
-                    file_part = MIMEAudio(self.get_content(msg_file),
-                                          _subtype=subtype)
-                else:
-                    file_part = MIMEBase(maintype, subtype)
-                    file_part.set_payload(self.get_content(msg_file))
-                    # Encode the payload using Base64
-                    encoders.encode_base64(msg)
-                # Set the filename parameter
-                file_part.add_header('Content-Disposition', 'attachment',
-                                     filename=f_name)
-                file_part.add_header('Content-Type', ctype, name=f_name)
-                msg.attach(file_part)
-        else:
-            raise Exception('Attachment files should be'
-                            'a dict in format {"filename":"filepath"}')
-
-    def get_content(self, msg_file):
-        """
-        Get content based on type, if content is a string do open first
-        else just read because it's a probably open file object
-
-        :param msg_file:
-        """
-        if isinstance(msg_file, str):
-            return open(msg_file, "rb").read()
-        else:
-            # just for safe seek to 0
-            msg_file.seek(0)
-            return msg_file.read()
-

File rhodecode/lib/utils.py

 import traceback
 import paste
 import beaker
+import tarfile
+import shutil
+from os.path import abspath
 from os.path import dirname as dn, join as jn
 
 from paste.script.command import Command, BadCommand
 from rhodecode.lib.caching_query import FromCache
 
 from rhodecode.model import meta
-from rhodecode.model.db import Repository, User, RhodeCodeUi, UserLog, RepoGroup, \
-    RhodeCodeSetting
+from rhodecode.model.db import Repository, User, RhodeCodeUi, \
+    UserLog, RepoGroup, RhodeCodeSetting
 
 log = logging.getLogger(__name__)
 
 
 
 def set_rhodecode_config(config):
-    """Updates pylons config with new settings from database
+    """
+    Updates pylons config with new settings from database
 
     :param config:
     """
 
 
 def invalidate_cache(cache_key, *args):
-    """Puts cache invalidation task into db for
+    """
+    Puts cache invalidation task into db for
     further global cache invalidation
     """
 
 
     @LazyProperty
     def raw_id(self):
-        """Returns raw string identifying this changeset, useful for web
+        """
+        Returns raw string identifying this changeset, useful for web
         representation.
         """
 
 
 
 def map_groups(groups):
-    """Checks for groups existence, and creates groups structures.
+    """
+    Checks for groups existence, and creates groups structures.
     It returns last group in structure
 
     :param groups: list of groups structure
     rm = RepoModel()
     user = sa.query(User).filter(User.admin == True).first()
     if user is None:
-        raise Exception('Missing administrative account !')    
+        raise Exception('Missing administrative account !')
     added = []
 
     for name, repo in initial_repo_list.items():
 
     return added, removed
 
-#set cache regions for beaker so celery can utilise it
+# set cache regions for beaker so celery can utilise it
 def add_cache(settings):
     cache_settings = {'regions': None}
     for key in settings.keys():
 
 
 def create_test_env(repos_test_path, config):
-    """Makes a fresh database and
+    """
+    Makes a fresh database and
     install test repository into tmp dir
     """
     from rhodecode.lib.db_manage import DbManage
     from rhodecode.tests import HG_REPO, TESTS_TMP_PATH
-    import tarfile
-    import shutil
-    from os.path import abspath
 
     # PART ONE create db
     dbconf = config['sqlalchemy.db1.url']

File rhodecode/model/comment.py

                                     {'commit_desc':desc, 'line':line},
                              h.url('changeset_home', repo_name=repo.repo_name,
                                    revision=revision,
-                                   anchor='comment-%s' % comment.comment_id
+                                   anchor='comment-%s' % comment.comment_id,
+                                   qualified=True,
                                    )
                              )
             body = text
                                    body=body, recipients=recipients,
                                    type_=Notification.TYPE_CHANGESET_COMMENT)
 
-            mention_recipients = set(self._extract_mentions(body)).difference(recipients)
+            mention_recipients = set(self._extract_mentions(body))\
+                                    .difference(recipients)
             if mention_recipients:
                 subj = _('[Mention]') + ' ' + subj
                 NotificationModel().create(created_by=user_id, subject=subj,
-                                    body = body, recipients = mention_recipients,
+                                    body=body,
+                                    recipients=mention_recipients,
                                     type_=Notification.TYPE_CHANGESET_COMMENT)
 
             self.sa.commit()

File rhodecode/model/db.py

 
     group_member = relationship('UsersGroupMember', cascade='all')
 
-    notifications = relationship('UserNotification')
+    notifications = relationship('UserNotification',)
 
     @property
     def full_contact(self):
     type_ = Column('type', Unicode(256))
 
     created_by_user = relationship('User')
-    notifications_to_users = relationship('UserNotification',
-        primaryjoin='Notification.notification_id==UserNotification.notification_id',
-        lazy='joined',
-        cascade="all, delete, delete-orphan")
+    notifications_to_users = relationship('UserNotification', lazy='joined',
+                                          cascade="all, delete, delete-orphan")
 
     @property
     def recipients(self):
         notification.subject = subject
         notification.body = body
         notification.type_ = type_
+        notification.created_on = datetime.datetime.now()
 
         for u in recipients:
             assoc = UserNotification()
     sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
 
     user = relationship('User', lazy="joined")
-    notification = relationship('Notification', lazy="joined", cascade='all')
+    notification = relationship('Notification', lazy="joined",
+                                order_by=lambda:Notification.created_on.desc(),
+                                cascade='all')
 
     def mark_as_read(self):
         self.read = True

File rhodecode/model/notification.py

 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import os
 import logging
 import traceback
+import datetime
 
+from pylons import config
 from pylons.i18n.translation import _
 
-from rhodecode.lib.helpers import age
-
+from rhodecode.lib import helpers as h
 from rhodecode.model import BaseModel
 from rhodecode.model.db import Notification, User, UserNotification
+from rhodecode.lib.celerylib import run_task
+from rhodecode.lib.celerylib.tasks import send_email
 
 log = logging.getLogger(__name__)
 
             if obj:
                 recipients_objs.append(obj)
         recipients_objs = set(recipients_objs)
-        return Notification.create(created_by=created_by_obj, subject=subject,
-                            body=body, recipients=recipients_objs,
-                            type_=type_)
+
+        notif = Notification.create(created_by=created_by_obj, subject=subject,
+                                    body=body, recipients=recipients_objs,
+                                    type_=type_)
+
+
+        # send email with notification
+        for rec in recipients_objs:
+            email_subject = NotificationModel().make_description(notif, False)
+            type_ = EmailNotificationModel.TYPE_CHANGESET_COMMENT
+            email_body = body
+            email_body_html = EmailNotificationModel()\
+                            .get_email_tmpl(type_, **{'subject':subject,
+                                                      'body':h.rst(body)})
+            run_task(send_email, rec.email, email_subject, email_body,
+                     email_body_html)
+
+        return notif
 
     def delete(self, user, notification):
         # we don't want to remove actual notification just the assignment
             notification = self.__get_notification(notification)
             user = self.__get_user(user)
             if notification and user:
-                obj = UserNotification.query().filter(UserNotification.user == user)\
-                    .filter(UserNotification.notification == notification).one()
+                obj = UserNotification.query()\
+                        .filter(UserNotification.user == user)\
+                        .filter(UserNotification.notification
+                                == notification)\
+                        .one()
                 self.sa.delete(obj)
                 return True
         except Exception:
             .filter(UserNotification.notification == notification)\
             .filter(UserNotification.user == user).scalar()
 
-    def make_description(self, notification):
+    def make_description(self, notification, show_age=True):
         """
         Creates a human readable description based on properties
         of notification object
         _map = {notification.TYPE_CHANGESET_COMMENT:_('commented on commit'),
                 notification.TYPE_MESSAGE:_('sent message'),
                 notification.TYPE_MENTION:_('mentioned you')}
+        DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
 
         tmpl = "%(user)s %(action)s %(when)s"
+        if show_age:
+            when = h.age(notification.created_on)
+        else:
+            DTF = lambda d: datetime.datetime.strftime(d, DATETIME_FORMAT)
+            when = DTF(notification.created_on)
         data = dict(user=notification.created_by_user.username,
                     action=_map[notification.type_],
-                    when=age(notification.created_on))
+                    when=when)
         return tmpl % data
+
+
+class EmailNotificationModel(BaseModel):
+
+    TYPE_CHANGESET_COMMENT = 'changeset_comment'
+    TYPE_PASSWORD_RESET = 'passoword_link'
+    TYPE_REGISTRATION = 'registration'
+    TYPE_DEFAULT = 'default'
+
+    def __init__(self):
+        self._template_root = config['pylons.paths']['templates'][0]
+
+        self.email_types = {
+            self.TYPE_CHANGESET_COMMENT:'email_templates/changeset_comment.html',
+            self.TYPE_PASSWORD_RESET:'email_templates/password_reset.html',
+            self.TYPE_REGISTRATION:'email_templates/registration.html',
+            self.TYPE_DEFAULT:'email_templates/default.html'
+        }
+
+    def get_email_tmpl(self, type_, **kwargs):
+        """
+        return generated template for email based on given type
+        
+        :param type_:
+        """
+        base = self.email_types.get(type_, self.TYPE_DEFAULT)
+
+        lookup = config['pylons.app_globals'].mako_lookup
+        email_template = lookup.get_template(base)
+        # translator inject
+        _kwargs = {'_':_}
+        _kwargs.update(kwargs)
+        log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
+        return email_template.render(**_kwargs)
+
+

File rhodecode/public/js/rhodecode.js

 		)
 );
 
+var _run_callbacks = function(callbacks){
+	if (callbacks !== undefined){
+		var _l = callbacks.length;
+	    for (var i=0;i<_l;i++){
+	    	var func = callbacks[i];
+	    	if(typeof(func)=='function'){
+	            try{
+	          	    func();
+	            }catch (err){};            		
+	    	}
+	    }
+	}		
+}
+
 /**
  * Partial Ajax Implementation
  * 
 	}
 };
 
-var deleteNotification = function(url, notification_id){
+var deleteNotification = function(url, notification_id,callbacks){
     var callback = { 
 		success:function(o){
 		    var obj = YUD.get(String("notification_"+notification_id));
-			obj.parentNode.removeChild(obj);
+		    if(obj.parentNode !== undefined){
+				obj.parentNode.removeChild(obj);
+			}
+			_run_callbacks(callbacks);
 		},
 	    failure:function(o){
 	        alert("error");

File rhodecode/templates/admin/notifications/notifications.html

               <span id="${notification.notification.notification_id}" class="delete-notification delete_icon action"></span>
             </div>
           </div>
-          <div class="notification-subject">${h.urlify_text(notification.notification.subject)}</div>
+          <div class="notification-subject">${h.literal(notification.notification.subject)}</div>
         </div>
       %endfor
     </div>

File rhodecode/templates/admin/notifications/show_notification.html

         </ul>            
     </div>
     <div class="table">
-      <div class="notification-header">
-        <div class="gravatar">
-            <img alt="gravatar" src="${h.gravatar_url(h.email(c.notification.created_by_user.email),24)}"/>
+      <div id="notification_${c.notification.notification_id}">
+        <div class="notification-header">
+          <div class="gravatar">
+              <img alt="gravatar" src="${h.gravatar_url(h.email(c.notification.created_by_user.email),24)}"/>
+          </div>
+          <div class="desc">
+              ${c.notification.description}
+          </div>
+          <div class="delete-notifications">
+            <span id="${c.notification.notification_id}" class="delete-notification delete_icon action"></span>
+          </div>
         </div>
-        <div class="desc">
-            ${c.notification.description}
-        </div>
-        <div class="delete-notifications">
-          <span id="${c.notification.notification_id}" class="delete_icon action"></span>
-        </div>
+        <div>${h.rst(c.notification.body)}</div>
       </div>
-      <div>${h.rst(c.notification.body)}</div>
     </div>
 </div>
 <script type="text/javascript">
 var url = "${url('notification', notification_id='__NOTIFICATION_ID__')}";
+var main = "${url('notifications')}";
    YUE.on(YUQ('.delete-notification'),'click',function(e){
        var notification_id = e.currentTarget.id;
-       deleteNotification(url,notification_id)
+       deleteNotification(url,notification_id,[function(){window.location=main}])
    })
 </script>
 </%def>  

File rhodecode/templates/email_templates/changeset_comment.html

+## -*- coding: utf-8 -*-
+<%inherit file="main.html"/>
+
+<h4>${subject}</h4>
+
+${body}

File rhodecode/templates/email_templates/default.html

+## -*- coding: utf-8 -*-
+<%inherit file="main.html"/>
+
+${body}

File rhodecode/templates/email_templates/main.html

+${self.body()}
+
+
+<div>
+--
+<br/>
+<br/>
+<b>${_('This is an notification from RhodeCode.')}</b>
+</div>

File rhodecode/templates/email_templates/password_reset.html

+## -*- coding: utf-8 -*-
+<%inherit file="main.html"/>
+
+Hello ${user}
+
+We received a request to create a new password for your account.
+
+You can generate it by clicking following URL:
+
+${reset_url}
+
+If you didn't request new password please ignore this email.