mailing: switched from homebrew lib to pyramid_mailer with python3 compatability
This commit is contained in:
parent
21e7eef112
commit
5a37a33119
14 changed files with 94 additions and 852 deletions
|
|
@ -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 ##
|
||||
|
|
|
|||
|
|
@ -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 ##
|
||||
|
|
|
|||
|
|
@ -19,9 +19,7 @@ Once configured you can check the settings for your |RCM| instance on the
|
|||
## 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 settings for your |RCM| instance on the
|
|||
#smtp_port =
|
||||
#smtp_use_tls = false
|
||||
#smtp_use_ssl = true
|
||||
## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
|
||||
#smtp_auth =
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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='', html_body='', email_config=None):
|
|||
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='', html_body='', email_config=None):
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 "<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, 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"))
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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()
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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()
|
||||
|
|
@ -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'), ''),
|
||||
|
||||
]
|
||||
%>
|
||||
<dl class="dl-horizontal settings">
|
||||
|
|
@ -54,7 +52,3 @@
|
|||
${h.end_form()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ##
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue