changeset 8696:9a0c41175e66

mail: use plain standard library for sending mails instead of rcmail Avoid a lot of custom complexity that adds no value. A few trivial lines from rcmail are inlined.
author Mads Kiilerich <mads@kiilerich.com>
date Mon, 12 Oct 2020 21:05:32 +0200
parents 9ca3ebee3fd8
children c1677e2d9e67
files kallithea/lib/celerylib/tasks.py kallithea/lib/rcmail/__init__.py kallithea/lib/rcmail/exceptions.py kallithea/lib/rcmail/message.py kallithea/lib/rcmail/response.py kallithea/lib/rcmail/smtp_mailer.py kallithea/lib/rcmail/utils.py kallithea/tests/other/test_mail.py
diffstat 7 files changed, 38 insertions(+), 754 deletions(-) [+]
line wrap: on
line diff
--- a/kallithea/lib/celerylib/tasks.py	Mon Oct 12 21:25:01 2020 +0200
+++ b/kallithea/lib/celerylib/tasks.py	Mon Oct 12 21:05:32 2020 +0200
@@ -26,8 +26,12 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
+import email.mime.multipart
+import email.mime.text
 import email.utils
 import os
+import smtplib
+import time
 import traceback
 from collections import OrderedDict
 from operator import itemgetter
@@ -41,7 +45,6 @@
 from kallithea.lib.helpers import person
 from kallithea.lib.hooks import log_create_repository
 from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
-from kallithea.lib.rcmail.smtp_mailer import SmtpMailer
 from kallithea.lib.utils import action_logger
 from kallithea.lib.utils2 import asbool, ascii_bytes
 from kallithea.lib.vcs.utils import author_email
@@ -309,10 +312,38 @@
         log.warning(logmsg)
         return False
 
+    msg = email.mime.multipart.MIMEMultipart('alternative')
+    msg['Subject'] = subject
+    msg['From'] = app_email_from  # fallback - might be overridden by a header
+    msg['To'] = ', '.join(recipients)
+    msg['Date'] = email.utils.formatdate(time.time())
+
+    for key, value in headers.items():
+        msg[key] = value
+
+    msg.attach(email.mime.text.MIMEText(body, 'plain'))
+    msg.attach(email.mime.text.MIMEText(html_body, 'html'))
+
     try:
-        m = SmtpMailer(app_email_from, smtp_username, smtp_password, smtp_server, smtp_auth,
-                       smtp_port, smtp_use_ssl, smtp_use_tls)
-        m.send(recipients, subject, body, html_body, headers=headers)
+        if smtp_use_ssl:
+            smtp_serv = smtplib.SMTP_SSL(smtp_server, smtp_port)
+        else:
+            smtp_serv = smtplib.SMTP(smtp_server, smtp_port)
+
+        if smtp_use_tls:
+            smtp_serv.starttls()
+
+        if smtp_auth:
+            smtp_serv.ehlo()  # populate esmtp_features
+            smtp_serv.esmtp_features["auth"] = smtp_auth
+
+        if smtp_username and smtp_password:
+            smtp_serv.login(smtp_username, smtp_password)
+
+        smtp_serv.sendmail(app_email_from, recipients, msg.as_string())
+        smtp_serv.quit()
+
+        log.info('Mail was sent to: %s' % recipients)
     except:
         log.error('Mail sending failed')
         log.error(traceback.format_exc())
--- a/kallithea/lib/rcmail/exceptions.py	Mon Oct 12 21:25:01 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -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.
-    """
--- a/kallithea/lib/rcmail/message.py	Mon Oct 12 21:25:01 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,152 +0,0 @@
-from kallithea.lib.rcmail.exceptions import BadHeaders, InvalidMessage
-from kallithea.lib.rcmail.response import MailResponse
-
-
-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 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)
--- a/kallithea/lib/rcmail/response.py	Mon Oct 12 21:25:01 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,456 +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 mimetypes
-import os
-import string
-from email import encoders
-from email.charset import Charset
-from email.mime.base import MIMEBase
-from email.utils import parseaddr
-
-
-ADDRESS_HEADERS_WHITELIST = ['From', 'To', 'Delivered-To', 'Cc']
-DEFAULT_ENCODING = "utf-8"
-
-def VALUE_IS_EMAIL_ADDRESS(v):
-    return '@' 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 __bool__(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@example.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:
-                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, 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 e:  # pragma: no cover
-        raise EncodingError("Content-Type malformed, not allowed: %r; "
-                            "%r (Python ERROR: %s)" %
-                            (ctype, params, e.args[0]))
-
-    for k in mail.keys():
-        if k in ADDRESS_HEADERS_WHITELIST:
-            out[k] = header_to_mime_encoding(
-                                         mail[k],
-                                         not_email=False,
-                                         separator=separator
-                                     )
-        else:
-            out[k] = 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 isinstance(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:
-        value.encode("ascii")
-        return value
-    except UnicodeError:
-        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), address)
-
-        return encoder.header_encode(value)
--- a/kallithea/lib/rcmail/smtp_mailer.py	Mon Oct 12 21:25:01 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,106 +0,0 @@
-# -*- coding: utf-8 -*-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-"""
-kallithea.lib.rcmail.smtp_mailer
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Simple smtp mailer used in Kallithea
-
-This file was forked by the Kallithea project in July 2014.
-Original author and date, and relevant copyright and licensing information is below:
-:created_on: Sep 13, 2010
-:author: marcink
-:copyright: (c) 2013 RhodeCode GmbH, and others.
-:license: GPLv3, see LICENSE.md for more details.
-"""
-
-import logging
-import smtplib
-import time
-from email.utils import formatdate
-from ssl import SSLError
-
-from kallithea.lib.rcmail.message import Message
-from kallithea.lib.rcmail.utils import DNS_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 send(self, recipients=None, subject='', body='', html='',
-             attachment_files=None, headers=None):
-        recipients = recipients or []
-        if isinstance(recipients, str):
-            recipients = [recipients]
-        if headers is None:
-            headers = {}
-        headers.setdefault('Date', formatdate(time.time()))
-        msg = Message(subject, recipients, body, html, self.mail_from,
-                      recipients_separator=", ", extra_headers=headers)
-        raw_msg = msg.to_message()
-
-        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, list(msg.send_to), raw_msg.as_string())
-        logging.info('MAIL SENT TO: %s' % recipients)
-
-        try:
-            smtp_serv.quit()
-        except SSLError:
-            # SSL error might sometimes be raised in tls connections on closing
-            smtp_serv.close()
--- a/kallithea/lib/rcmail/utils.py	Mon Oct 12 21:25:01 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-"""
-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()
--- a/kallithea/tests/other/test_mail.py	Mon Oct 12 21:25:01 2020 +0200
+++ b/kallithea/tests/other/test_mail.py	Mon Oct 12 21:05:32 2020 +0200
@@ -10,7 +10,7 @@
 class smtplib_mock(object):
 
     @classmethod
-    def SMTP(cls, server, port, local_hostname):
+    def SMTP(cls, server, port):
         return smtplib_mock()
 
     def ehlo(self):
@@ -25,7 +25,7 @@
         smtplib_mock.lastmsg = msg
 
 
-@mock.patch('kallithea.lib.rcmail.smtp_mailer.smtplib', smtplib_mock)
+@mock.patch('kallithea.lib.celerylib.tasks.smtplib', smtplib_mock)
 class TestMail(base.TestController):
 
     def test_send_mail_trivial(self):
@@ -191,6 +191,6 @@
         assert 'Subject: %s' % subject in smtplib_mock.lastmsg
         assert body in smtplib_mock.lastmsg
         assert html_body in smtplib_mock.lastmsg
-        assert 'Extra: yes' in smtplib_mock.lastmsg
+        assert 'extra: yes' in smtplib_mock.lastmsg
         # verify that headers dict hasn't mutated by send_email
         assert headers == {'extra': 'yes'}