# HG changeset patch # User Marcin Kuzminski # Date 2018-10-03 16:47:58 # Node ID d517813a290fea9edc0c569d5c87a5efb47ac8b2 # Parent f3f66eb3c970c8c3b5172647fc2c70324796201d mailing: switched from homebrew lib to pyramid_mailer with python3 compatability diff --git a/configs/development.ini b/configs/development.ini --- a/configs/development.ini +++ b/configs/development.ini @@ -26,8 +26,6 @@ debug = true #smtp_port = #smtp_use_tls = false #smtp_use_ssl = true -## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.) -#smtp_auth = [server:main] ## COMMON ## diff --git a/configs/production.ini b/configs/production.ini --- a/configs/production.ini +++ b/configs/production.ini @@ -26,8 +26,6 @@ debug = true #smtp_port = #smtp_use_tls = false #smtp_use_ssl = true -## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.) -#smtp_auth = [server:main] ## COMMON ## diff --git a/docs/install/setup-email.rst b/docs/install/setup-email.rst --- a/docs/install/setup-email.rst +++ b/docs/install/setup-email.rst @@ -19,9 +19,7 @@ Once configured you can check the settin ## Additionally these settings will be used by the RhodeCode mailing system ## ################################################################################ #email_to = admin@localhost - #error_email_from = paste_error@localhost #app_email_from = rhodecode-noreply@localhost - #error_message = #email_prefix = [RhodeCode] #smtp_server = mail.server.com @@ -30,5 +28,3 @@ Once configured you can check the settin #smtp_port = #smtp_use_tls = false #smtp_use_ssl = true - ## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.) - #smtp_auth = diff --git a/pkgs/python-packages.nix b/pkgs/python-packages.nix --- a/pkgs/python-packages.nix +++ b/pkgs/python-packages.nix @@ -1368,6 +1368,22 @@ self: super: { license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; }; }; + "pyramid-mailer" = super.buildPythonPackage { + name = "pyramid-mailer-0.15.1"; + doCheck = false; + propagatedBuildInputs = [ + self."pyramid" + self."repoze.sendmail" + self."transaction" + ]; + src = fetchurl { + url = "https://files.pythonhosted.org/packages/a0/f2/6febf5459dff4d7e653314d575469ad2e11b9d2af2c3606360e1c67202f2/pyramid_mailer-0.15.1.tar.gz"; + sha256 = "16vg8jb203jgb7b0hd6wllfqvp542qh2ry1gjai2m6qpv5agy2pc"; + }; + meta = { + license = [ pkgs.lib.licenses.bsdOriginal ]; + }; + }; "pyramid-mako" = super.buildPythonPackage { name = "pyramid-mako-1.0.2"; doCheck = false; @@ -1596,6 +1612,22 @@ self: super: { license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; }; }; + "repoze.sendmail" = super.buildPythonPackage { + name = "repoze.sendmail-4.4.1"; + doCheck = false; + propagatedBuildInputs = [ + self."setuptools" + self."zope.interface" + self."transaction" + ]; + src = fetchurl { + url = "https://files.pythonhosted.org/packages/12/4e/8ef1fd5c42765d712427b9c391419a77bd48877886d2cbc5e9f23c8cad9b/repoze.sendmail-4.4.1.tar.gz"; + sha256 = "096ln02jr2afk7ab9j2czxqv2ryqq7m86ah572nqplx52iws73ks"; + }; + meta = { + license = [ pkgs.lib.licenses.zpl21 ]; + }; + }; "requests" = super.buildPythonPackage { name = "requests-2.9.1"; doCheck = false; @@ -1685,6 +1717,7 @@ self: super: { self."pyramid-jinja2" self."pyramid-mako" self."pyramid" + self."pyramid-mailer" self."pysqlite" self."python-dateutil" self."python-ldap" @@ -1970,6 +2003,20 @@ self: super: { license = [ pkgs.lib.licenses.bsdOriginal ]; }; }; + "transaction" = super.buildPythonPackage { + name = "transaction-2.2.1"; + doCheck = false; + propagatedBuildInputs = [ + self."zope.interface" + ]; + src = fetchurl { + url = "https://files.pythonhosted.org/packages/a6/5e/09da91cb9373c73aae41721e5571c47db72fa9e11b259ca8fd3b01e306e9/transaction-2.2.1.tar.gz"; + sha256 = "0ykcfx5mw76z8wwg6a68ahr52q3qa75hky1xx9axbr9pwiq2097j"; + }; + meta = { + license = [ pkgs.lib.licenses.zpl21 ]; + }; + }; "translationstring" = super.buildPythonPackage { name = "translationstring-1.3"; doCheck = false; diff --git a/requirements.txt b/requirements.txt --- a/requirements.txt +++ b/requirements.txt @@ -61,6 +61,7 @@ pyramid-debugtoolbar==4.4.0 pyramid-jinja2==2.7 pyramid-mako==1.0.2 pyramid==1.9.2 +pyramid_mailer==0.15.1 pysqlite==2.8.3 python-dateutil python-ldap==3.1.0 diff --git a/rhodecode/lib/celerylib/tasks.py b/rhodecode/lib/celerylib/tasks.py --- a/rhodecode/lib/celerylib/tasks.py +++ b/rhodecode/lib/celerylib/tasks.py @@ -26,11 +26,13 @@ by celery daemon import os import time +from pyramid_mailer.mailer import Mailer +from pyramid_mailer.message import Message + import rhodecode from rhodecode.lib import audit_logger from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask from rhodecode.lib.hooks_base import log_create_repository -from rhodecode.lib.rcmail.smtp_mailer import SmtpMailer from rhodecode.lib.utils2 import safe_int, str2bool from rhodecode.model.db import Session, IntegrityError, Repository, User, true @@ -58,7 +60,11 @@ def send_email(recipients, subject, body return False subject = "%s %s" % (email_config.get('email_prefix', ''), subject) - if not recipients: + + if recipients: + if isinstance(recipients, basestring): + recipients = recipients.split(',') + else: # if recipients are not defined we send to email_config + all admins admins = [] for u in User.query().filter(User.admin == true()).all(): @@ -70,19 +76,44 @@ def send_email(recipients, subject, body recipients += [config_email] recipients += admins - mail_from = email_config.get('app_email_from', 'RhodeCode') - user = email_config.get('smtp_username') - passwd = email_config.get('smtp_password') - mail_port = email_config.get('smtp_port') - tls = str2bool(email_config.get('smtp_use_tls')) - ssl = str2bool(email_config.get('smtp_use_ssl')) - debug = str2bool(email_config.get('debug')) - smtp_auth = email_config.get('smtp_auth') + # translate our LEGACY config into the one that pyramid_mailer supports + email_conf = dict( + host=mail_server, + port=email_config.get('smtp_port'), + username=email_config.get('smtp_username'), + password=email_config.get('smtp_password'), + + tls=str2bool(email_config.get('smtp_use_tls')), + ssl=str2bool(email_config.get('smtp_use_ssl')), + + # SSL key file + # keyfile='', + + # SSL certificate file + # certfile='', + + # Location of maildir + # queue_path='', + + default_sender=email_config.get('app_email_from', 'RhodeCode'), + + debug=str2bool(email_config.get('smtp_debug')), + # /usr/sbin/sendmail Sendmail executable + # sendmail_app='', + + # {sendmail_app} -t -i -f {sender} Template for sendmail execution + # sendmail_template='', + ) try: - m = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth, - mail_port, ssl, tls, debug=debug) - m.send(recipients, subject, body, html_body) + mailer = Mailer(**email_conf) + + message = Message(subject=subject, + sender=email_conf['default_sender'], + recipients=recipients, + body=body, html=html_body) + mailer.send_immediately(message) + except Exception: log.exception('Mail sending failed') return False diff --git a/rhodecode/lib/rcmail/__init__.py b/rhodecode/lib/rcmail/__init__.py deleted file mode 100644 diff --git a/rhodecode/lib/rcmail/exceptions.py b/rhodecode/lib/rcmail/exceptions.py deleted file mode 100644 --- a/rhodecode/lib/rcmail/exceptions.py +++ /dev/null @@ -1,13 +0,0 @@ - - -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 --git a/rhodecode/lib/rcmail/message.py b/rhodecode/lib/rcmail/message.py deleted file mode 100644 --- a/rhodecode/lib/rcmail/message.py +++ /dev/null @@ -1,188 +0,0 @@ -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 - :param recipients_separator: alternative separator for any of - 'From', 'To', 'Delivered-To', 'Cc', 'Bcc' fields - """ - - def __init__(self, - subject=None, - recipients=None, - body=None, - html=None, - sender=None, - cc=None, - bcc=None, - extra_headers=None, - attachments=None, - recipients_separator="; "): - - 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 {} - - self.recipients_separator = recipients_separator - - @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, - separator=self.recipients_separator) - - 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 _get_headers(self): - headers = [self.subject, self.sender] - headers += list(self.send_to) - headers += self.extra_headers.values() - return headers - - def is_bad_headers(self): - """ - Checks for bad headers i.e. newlines in subject, sender or recipients. - """ - - headers = self._get_headers() - - 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(): - headers = self._get_headers() - raise BadHeaders(headers) - - 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 --git a/rhodecode/lib/rcmail/response.py b/rhodecode/lib/rcmail/response.py deleted file mode 100644 --- a/rhodecode/lib/rcmail/response.py +++ /dev/null @@ -1,454 +0,0 @@ -# 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'] -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 is not 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, - separator="; "): - self.Body = Body - self.Html = Html - self.base = MailBase([('To', To), ('From', From), ('Subject', Subject)]) - self.multipart = self.Body and self.Html - self.attachments = [] - self.separator = separator - - 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: - with open(filename) as f: - data = f.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, separator=self.separator) - - 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, separator="; "): - """ - 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 as 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], - not_email=False, - separator=separator - ) - 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 is 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, separator=", "): - if not value: - return "" - - encoder = Charset(DEFAULT_ENCODING) - if type(value) == list: - return separator.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 not_email 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 --git a/rhodecode/lib/rcmail/smtp_mailer.py b/rhodecode/lib/rcmail/smtp_mailer.py deleted file mode 100644 --- a/rhodecode/lib/rcmail/smtp_mailer.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2013-2018 RhodeCode GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License, version 3 -# (only), as published by the Free Software Foundation. -# -# 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 Affero General Public License -# along with this program. If not, see . -# -# This program is dual-licensed. If you wish to learn more about the -# RhodeCode Enterprise Edition, including its added features, Support services, -# and proprietary license terms, please see https://rhodecode.com/licenses/ - -""" -Simple smtp mailer used in RhodeCode -""" - -import time -import logging -import socket -from email.utils import formatdate - -from rhodecode.lib.rcmail.message import Message -from rhodecode.lib.rcmail.utils import DNS_NAME - -log = logging.getLogger(__name__) - - -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 _get_smptlib(self): - # patch the output - import smtplib - - class StderrLogger(object): - - def write(self, message): - log.debug(message) - - org_stderr = smtplib.stderr - smtplib.stderr = StderrLogger() - return smtplib - - def send(self, recipients=None, subject='', body='', html='', - attachment_files=None): - recipients = recipients or [] - if isinstance(recipients, basestring): - recipients = [recipients] - headers = { - 'Date': formatdate(time.time()) - } - msg = Message(subject, recipients, body, html, self.mail_from, - recipients_separator=", ", extra_headers=headers) - raw_msg = msg.to_message() - - # patched smtplib without stderr - smtplib = self._get_smptlib() - if self.ssl: - smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port, - local_hostname=DNS_NAME.get_fqdn()) - else: - smtp_serv = smtplib.SMTP(self.mail_server, self.mail_port, - local_hostname=DNS_NAME.get_fqdn()) - - 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()) - log.info('email sent to: %s', recipients) - - try: - smtp_serv.quit() - except socket.sslerror: - # sslerror is raised in tls connections on closing sometimes - smtp_serv.close() diff --git a/rhodecode/lib/rcmail/utils.py b/rhodecode/lib/rcmail/utils.py deleted file mode 100644 --- a/rhodecode/lib/rcmail/utils.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2010-2018 RhodeCode GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License, version 3 -# (only), as published by the Free Software Foundation. -# -# 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 Affero General Public License -# along with this program. If not, see . -# -# This program is dual-licensed. If you wish to learn more about the -# RhodeCode Enterprise Edition, including its added features, Support services, -# and proprietary license terms, please see https://rhodecode.com/licenses/ - -""" -Email message and email sending related helper functions. -""" - -import socket - - -# Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of -# seconds, which slows down the restart of the server. -class CachedDnsName(object): - def __str__(self): - return self.get_fqdn() - - def get_fqdn(self): - if not hasattr(self, '_fqdn'): - self._fqdn = socket.getfqdn() - return self._fqdn - -DNS_NAME = CachedDnsName() diff --git a/rhodecode/templates/admin/settings/settings_email.mako b/rhodecode/templates/admin/settings/settings_email.mako --- a/rhodecode/templates/admin/settings/settings_email.mako +++ b/rhodecode/templates/admin/settings/settings_email.mako @@ -6,9 +6,7 @@ <% elems = [ (_('Email prefix'), c.rhodecode_ini.get('email_prefix'), ''), - (_('RhodeCode email from'), c.rhodecode_ini.get('app_email_from'), ''), - (_('Error email from'), c.rhodecode_ini.get('error_email_from'), ''), - (_('Error email recipients'), c.rhodecode_ini.get('email_to'), ''), + (_('Email from'), c.rhodecode_ini.get('app_email_from'), ''), (_('SMTP server'), c.rhodecode_ini.get('smtp_server'), ''), (_('SMTP username'), c.rhodecode_ini.get('smtp_username'), ''), @@ -17,7 +15,7 @@ (_('SMTP use TLS'), c.rhodecode_ini.get('smtp_use_tls'), ''), (_('SMTP use SSL'), c.rhodecode_ini.get('smtp_use_ssl'), ''), - (_('SMTP auth'), c.rhodecode_ini.get('smtp_auth'), ''), + ] %>
@@ -54,7 +52,3 @@ ${h.end_form()} - - - - diff --git a/rhodecode/tests/rhodecode.ini b/rhodecode/tests/rhodecode.ini --- a/rhodecode/tests/rhodecode.ini +++ b/rhodecode/tests/rhodecode.ini @@ -25,21 +25,12 @@ debug = true ## note: using appenlight for error handling doesn't need this to be uncommented #email_to = admin@localhost -## in case of Application errors, sent an error email form -#error_email_from = rhodecode_error@localhost - -## additional error message to be send in case of server crash -#error_message = - - #smtp_server = mail.server.com #smtp_username = #smtp_password = #smtp_port = #smtp_use_tls = false #smtp_use_ssl = true -## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.) -#smtp_auth = [server:main] ## COMMON ##