# HG changeset patch # User Marcin Kuzminski # Date 1322235702 -7200 # Node ID 7ff304d3028f75dbcba25f5281f39135ef451dfa # Parent 7d1fc253549e44537852c2217774be1180a19d66 Notification fixes - email prefix added to .ini files - html templates emails - rewrote email system to use some parts from pyramid_mailer diff -r 7d1fc253549e -r 7ff304d3028f development.ini --- a/development.ini Wed Nov 23 22:46:14 2011 +0200 +++ b/development.ini Fri Nov 25 17:41:42 2011 +0200 @@ -17,6 +17,7 @@ #error_email_from = paste_error@localhost #app_email_from = rhodecode-noreply@localhost #error_message = +#email_prefix = [RhodeCode] #smtp_server = mail.server.com #smtp_username = diff -r 7d1fc253549e -r 7ff304d3028f production.ini --- a/production.ini Wed Nov 23 22:46:14 2011 +0200 +++ b/production.ini Fri Nov 25 17:41:42 2011 +0200 @@ -17,6 +17,7 @@ #error_email_from = paste_error@localhost #app_email_from = rhodecode-noreply@localhost #error_message = +#email_prefix = [RhodeCode] #smtp_server = mail.server.com #smtp_username = diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/config/deployment.ini_tmpl --- a/rhodecode/config/deployment.ini_tmpl Wed Nov 23 22:46:14 2011 +0200 +++ b/rhodecode/config/deployment.ini_tmpl Fri Nov 25 17:41:42 2011 +0200 @@ -17,6 +17,7 @@ #error_email_from = paste_error@localhost #app_email_from = rhodecode-noreply@localhost #error_message = +#email_prefix = [RhodeCode] #smtp_server = mail.server.com #smtp_username = diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/controllers/admin/settings.py --- a/rhodecode/controllers/admin/settings.py Wed Nov 23 22:46:14 2011 +0200 +++ b/rhodecode/controllers/admin/settings.py Fri Nov 25 17:41:42 2011 +0200 @@ -47,7 +47,8 @@ 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__) @@ -261,9 +262,12 @@ 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')) diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/lib/celerylib/tasks.py --- a/rhodecode/lib/celerylib/tasks.py Wed Nov 23 22:46:14 2011 +0200 +++ b/rhodecode/lib/celerylib/tasks.py Fri Nov 25 17:41:42 2011 +0200 @@ -28,7 +28,7 @@ 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 @@ -37,11 +37,12 @@ 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 @@ -53,6 +54,7 @@ from sqlalchemy import engine_from_config + add_cache(config) __all__ = ['whoosh_index', 'get_commits_stats', @@ -68,6 +70,16 @@ 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() @@ -88,10 +100,7 @@ @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) @@ -246,39 +255,27 @@ @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 @@ -286,18 +283,14 @@ @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: @@ -308,13 +301,12 @@ 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) @@ -326,7 +318,7 @@ @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. @@ -334,20 +326,19 @@ 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') @@ -360,7 +351,7 @@ 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()) @@ -370,14 +361,11 @@ @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'] diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/lib/rcmail/__init__.py diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/lib/rcmail/exceptions.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/rcmail/exceptions.py Fri Nov 25 17:41:42 2011 +0200 @@ -0,0 +1,11 @@ + +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. + """ diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/lib/rcmail/message.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/rcmail/message.py Fri Nov 25 17:41:42 2011 +0200 @@ -0,0 +1,182 @@ +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) + + diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/lib/rcmail/response.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/rcmail/response.py Fri Nov 25 17:41:42 2011 +0200 @@ -0,0 +1,438 @@ +# 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 "" % ( + 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")) diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/lib/rcmail/smtp_mailer.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/rcmail/smtp_mailer.py Fri Nov 25 17:41:42 2011 +0200 @@ -0,0 +1,93 @@ +# -*- 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 + :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 . + +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 diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/lib/smtp_mailer.py --- a/rhodecode/lib/smtp_mailer.py Wed Nov 23 22:46:14 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,166 +0,0 @@ -# -*- coding: utf-8 -*- -""" - rhodecode.lib.smtp_mailer - ~~~~~~~~~~~~~~~~~~~~~~~~~ - - Simple smtp mailer used in RhodeCode - - :created_on: Sep 13, 2010 - :copyright: (C) 2009-2011 Marcin Kuzminski - :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 . - -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() - diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/lib/utils.py --- a/rhodecode/lib/utils.py Wed Nov 23 22:46:14 2011 +0200 +++ b/rhodecode/lib/utils.py Fri Nov 25 17:41:42 2011 +0200 @@ -29,6 +29,9 @@ 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 @@ -46,8 +49,8 @@ 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__) @@ -283,7 +286,8 @@ def set_rhodecode_config(config): - """Updates pylons config with new settings from database + """ + Updates pylons config with new settings from database :param config: """ @@ -294,7 +298,8 @@ def invalidate_cache(cache_key, *args): - """Puts cache invalidation task into db for + """ + Puts cache invalidation task into db for further global cache invalidation """ @@ -323,7 +328,8 @@ @LazyProperty def raw_id(self): - """Returns raw string identifying this changeset, useful for web + """ + Returns raw string identifying this changeset, useful for web representation. """ @@ -348,7 +354,8 @@ 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 @@ -387,7 +394,7 @@ 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(): @@ -418,7 +425,7 @@ 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(): @@ -477,14 +484,12 @@ 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'] diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/model/comment.py --- a/rhodecode/model/comment.py Wed Nov 23 22:46:14 2011 +0200 +++ b/rhodecode/model/comment.py Fri Nov 25 17:41:42 2011 +0200 @@ -87,7 +87,8 @@ {'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 @@ -99,11 +100,13 @@ 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() diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/model/db.py --- a/rhodecode/model/db.py Wed Nov 23 22:46:14 2011 +0200 +++ b/rhodecode/model/db.py Fri Nov 25 17:41:42 2011 +0200 @@ -283,7 +283,7 @@ group_member = relationship('UsersGroupMember', cascade='all') - notifications = relationship('UserNotification') + notifications = relationship('UserNotification',) @property def full_contact(self): @@ -1150,10 +1150,8 @@ 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): @@ -1170,6 +1168,7 @@ notification.subject = subject notification.body = body notification.type_ = type_ + notification.created_on = datetime.datetime.now() for u in recipients: assoc = UserNotification() @@ -1193,7 +1192,9 @@ 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 diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/model/notification.py --- a/rhodecode/model/notification.py Wed Nov 23 22:46:14 2011 +0200 +++ b/rhodecode/model/notification.py Fri Nov 25 17:41:42 2011 +0200 @@ -24,15 +24,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +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__) @@ -82,9 +86,24 @@ 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 @@ -92,8 +111,11 @@ 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: @@ -124,7 +146,7 @@ .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 @@ -133,9 +155,51 @@ _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) + + diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/public/js/rhodecode.js --- a/rhodecode/public/js/rhodecode.js Wed Nov 23 22:46:14 2011 +0200 +++ b/rhodecode/public/js/rhodecode.js Fri Nov 25 17:41:42 2011 +0200 @@ -131,6 +131,20 @@ ) ); +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 * @@ -564,11 +578,14 @@ } }; -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"); diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/templates/admin/notifications/notifications.html --- a/rhodecode/templates/admin/notifications/notifications.html Wed Nov 23 22:46:14 2011 +0200 +++ b/rhodecode/templates/admin/notifications/notifications.html Fri Nov 25 17:41:42 2011 +0200 @@ -42,7 +42,7 @@ -
${h.urlify_text(notification.notification.subject)}
+
${h.literal(notification.notification.subject)}
%endfor diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/templates/admin/notifications/show_notification.html --- a/rhodecode/templates/admin/notifications/show_notification.html Wed Nov 23 22:46:14 2011 +0200 +++ b/rhodecode/templates/admin/notifications/show_notification.html Fri Nov 25 17:41:42 2011 +0200 @@ -27,25 +27,28 @@
-
-
- gravatar +
+
+
+ gravatar +
+
+ ${c.notification.description} +
+
+ +
-
- ${c.notification.description} -
-
- -
+
${h.rst(c.notification.body)}
-
${h.rst(c.notification.body)}
diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/templates/email_templates/changeset_comment.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/templates/email_templates/changeset_comment.html Fri Nov 25 17:41:42 2011 +0200 @@ -0,0 +1,6 @@ +## -*- coding: utf-8 -*- +<%inherit file="main.html"/> + +

${subject}

+ +${body} \ No newline at end of file diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/templates/email_templates/default.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/templates/email_templates/default.html Fri Nov 25 17:41:42 2011 +0200 @@ -0,0 +1,4 @@ +## -*- coding: utf-8 -*- +<%inherit file="main.html"/> + +${body} \ No newline at end of file diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/templates/email_templates/main.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/templates/email_templates/main.html Fri Nov 25 17:41:42 2011 +0200 @@ -0,0 +1,9 @@ +${self.body()} + + +
+-- +
+
+${_('This is an notification from RhodeCode.')} +
\ No newline at end of file diff -r 7d1fc253549e -r 7ff304d3028f rhodecode/templates/email_templates/password_reset.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/templates/email_templates/password_reset.html Fri Nov 25 17:41:42 2011 +0200 @@ -0,0 +1,12 @@ +## -*- 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. \ No newline at end of file