smtp backend refactoring
This commit is contained in:
parent
7618f71175
commit
37d0b67dd0
|
@ -0,0 +1,57 @@
|
|||
# encoding: utf-8
|
||||
|
||||
|
||||
class Response(object):
|
||||
|
||||
def __init__(self, exception=None, backend=None):
|
||||
self.backend = backend
|
||||
self.set_exception(exception)
|
||||
self.from_addr = None
|
||||
self.to_addrs = None
|
||||
self._finished = False
|
||||
|
||||
def set_exception(self, exc):
|
||||
self._exc = exc
|
||||
|
||||
def raise_if_needed(self):
|
||||
if self._exc:
|
||||
raise self._exc
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
return self._exc
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return self._finished
|
||||
|
||||
|
||||
class SMTPResponse(Response):
|
||||
|
||||
def __init__(self, exception=None, backend=None):
|
||||
|
||||
super(SMTPResponse, self).__init__(exception=exception, backend=backend)
|
||||
|
||||
self.responses = []
|
||||
|
||||
self.esmtp_opts = None
|
||||
self.rcpt_options = None
|
||||
|
||||
self.status_code = None
|
||||
self.status_text = None
|
||||
self.last_command = None
|
||||
|
||||
def set_status(self, command, code, text, **kwargs):
|
||||
self.responses.append([command, code, text, kwargs])
|
||||
self.status_code = code
|
||||
self.status_text = text
|
||||
self.last_command = command
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return self._finished and self.status_code and self.status_code == 250
|
||||
|
||||
def __repr__(self):
|
||||
return "<emails.backend.SMTPResponse status_code=%s status_text=%s>" % (self.status_code.__repr__(),
|
||||
self.status_text.__repr__())
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
# encoding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__all__ = ['SMTPBackend']
|
||||
__all__ = ['SMTPBackend', ]
|
||||
|
||||
import smtplib
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from .client import SMTPResponse, SMTPClientWithResponse, SMTPClientWithResponse_SSL
|
||||
from ...compat import to_bytes
|
||||
from ..response import SMTPResponse
|
||||
from .client import SMTPClientWithResponse, SMTPClientWithResponse_SSL
|
||||
from ...utils import DNS_NAME
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -16,11 +16,7 @@ logger = logging.getLogger(__name__)
|
|||
class SMTPBackend:
|
||||
|
||||
"""
|
||||
SMTPSender is a wrapper for smtplib.SMTP class.
|
||||
Differences are:
|
||||
a) it transparently uses SSL or no-SSL connection
|
||||
b) sendmail method sends only one message, but returns more information
|
||||
about server response (i.e. response code)
|
||||
SMTPBackend manages a smtp connection.
|
||||
"""
|
||||
|
||||
DEFAULT_SOCKET_TIMEOUT = 5
|
||||
|
@ -29,11 +25,7 @@ class SMTPBackend:
|
|||
connection_ssl_cls = SMTPClientWithResponse_SSL
|
||||
response_cls = SMTPResponse
|
||||
|
||||
|
||||
def __init__(self,
|
||||
ssl=False,
|
||||
fail_silently=True,
|
||||
**kwargs):
|
||||
def __init__(self, ssl=False, fail_silently=True, **kwargs):
|
||||
|
||||
self.smtp_cls = ssl and self.connection_ssl_cls or self.connection_cls
|
||||
|
||||
|
@ -44,82 +36,83 @@ class SMTPBackend:
|
|||
"ssl/tls are mutually exclusive, so only set "
|
||||
"one of those settings to True.")
|
||||
|
||||
if 'timeout' not in kwargs:
|
||||
kwargs['timeout'] = self.DEFAULT_SOCKET_TIMEOUT
|
||||
kwargs.setdefault('timeout', self.DEFAULT_SOCKET_TIMEOUT)
|
||||
kwargs.setdefault('local_hostname', DNS_NAME.get_fqdn())
|
||||
|
||||
self.smtp_cls_kwargs = kwargs
|
||||
|
||||
self.host = kwargs.get('host')
|
||||
self.port = kwargs.get('port')
|
||||
self.fail_silently = fail_silently
|
||||
self.connection = None
|
||||
#self.local_hostname=DNS_NAME.get_fqdn()
|
||||
|
||||
def open(self):
|
||||
if self.connection is None:
|
||||
self.connection = self.smtp_cls(parent=self, **self.smtp_cls_kwargs)
|
||||
self.connection.initialize()
|
||||
return self.connection
|
||||
self._client = None
|
||||
|
||||
def get_client(self):
|
||||
if self._client is None:
|
||||
self._client = self.smtp_cls(parent=self, **self.smtp_cls_kwargs)
|
||||
return self._client
|
||||
|
||||
def close(self):
|
||||
"""Closes the connection to the email server."""
|
||||
|
||||
if self.connection is None:
|
||||
return
|
||||
"""
|
||||
Closes the connection to the email server.
|
||||
"""
|
||||
|
||||
try:
|
||||
self.connection.close()
|
||||
except:
|
||||
if self.fail_silently:
|
||||
return
|
||||
raise
|
||||
finally:
|
||||
self.connection = None
|
||||
if self._client:
|
||||
try:
|
||||
self._client.close()
|
||||
except:
|
||||
if self.fail_silently:
|
||||
return
|
||||
raise
|
||||
finally:
|
||||
self._client = None
|
||||
|
||||
def make_response(self, exception=None):
|
||||
return self.response_cls(host=self.host, port=self.port, exception=exception)
|
||||
return self.response_cls(backend=self, exception=exception)
|
||||
|
||||
def retry_on_disconnect(self, func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except smtplib.SMTPServerDisconnected as e:
|
||||
# If server disconected, just connect again
|
||||
except smtplib.SMTPServerDisconnected:
|
||||
# If server disconected, clear old client
|
||||
logging.debug('SMTPServerDisconnected, retry once')
|
||||
self.close()
|
||||
self.open()
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
def _sendmail_on_connection(self, *args, **kwargs):
|
||||
return self.connection.sendmail(*args, **kwargs)
|
||||
def _send(self, **kwargs):
|
||||
|
||||
def sendmail(self, from_addr, to_addrs, msg, mail_options=[], rcpt_options=[]):
|
||||
try:
|
||||
client = self.get_client()
|
||||
except (IOError, smtplib.SMTPException) as exc:
|
||||
logger.exception("Error connecting smtp server")
|
||||
response = self.make_response(exception=exc)
|
||||
if not self.fail_silently:
|
||||
response.raise_if_needed()
|
||||
return response
|
||||
|
||||
return client.sendmail(**kwargs)
|
||||
|
||||
def sendmail(self, from_addr, to_addrs, msg, mail_options=None, rcpt_options=None):
|
||||
|
||||
if not to_addrs:
|
||||
return []
|
||||
return None
|
||||
|
||||
if not isinstance(to_addrs, (list, tuple)):
|
||||
to_addrs = [to_addrs, ]
|
||||
|
||||
try:
|
||||
self.open()
|
||||
except (IOError, smtplib.SMTPException) as e:
|
||||
logger.exception("Error connecting smtp server")
|
||||
response = self.make_response(exception=e)
|
||||
if not self.fail_silently:
|
||||
response.raise_if_needed()
|
||||
return [response, ]
|
||||
send = self.retry_on_disconnect(self._send)
|
||||
|
||||
_sendmail = self.retry_on_disconnect(self._sendmail_on_connection)
|
||||
|
||||
response = _sendmail(from_addr=from_addr,
|
||||
to_addrs=to_addrs,
|
||||
msg=to_bytes(msg.as_string(), 'utf-8'),
|
||||
mail_options=mail_options,
|
||||
rcpt_options=rcpt_options)
|
||||
response = send(from_addr=from_addr,
|
||||
to_addrs=to_addrs,
|
||||
msg=msg.as_string(),
|
||||
mail_options=mail_options,
|
||||
rcpt_options=rcpt_options)
|
||||
|
||||
if not self.fail_silently:
|
||||
[r.raise_if_needed() for r in response]
|
||||
response.raise_if_needed()
|
||||
|
||||
return response
|
||||
|
|
|
@ -1,91 +1,63 @@
|
|||
# encoding: utf-8
|
||||
__all__ = ['SMTPResponse', 'SMTPClientWithResponse', 'SMTPClientWithResponse_SSL']
|
||||
|
||||
from smtplib import _have_ssl, SMTP
|
||||
__all__ = ['SMTPClientWithResponse', 'SMTPClientWithResponse_SSL']
|
||||
|
||||
import smtplib
|
||||
from smtplib import _have_ssl, SMTP
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SMTPResponse(object):
|
||||
|
||||
def __init__(self, host=None, port=None, ssl=None, exception=None):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.ssl = ssl
|
||||
self.responses = []
|
||||
self.exception = exception
|
||||
self.success = None
|
||||
self.from_addr = None
|
||||
self.esmtp_opts = None
|
||||
self.rcpt_options = None
|
||||
self.to_addr = None
|
||||
self.status_code = None
|
||||
self.status_text = None
|
||||
self.last_command = None
|
||||
|
||||
def set_status(self, command, code, text):
|
||||
self.responses.append([command, code, text])
|
||||
self.status_code = code
|
||||
self.status_text = text
|
||||
self.last_command = command
|
||||
|
||||
def set_exception(self, exc):
|
||||
self.exception = exc
|
||||
|
||||
def raise_if_needed(self):
|
||||
if self.exception:
|
||||
raise self.exception
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
return self.exception
|
||||
|
||||
def __repr__(self):
|
||||
return "<emails.smtp.SMTPResponse status_code=%s status_text=%s>" % (self.status_code.__repr__(),
|
||||
self.status_text.__repr__())
|
||||
|
||||
|
||||
class SMTPClientWithResponse(SMTP):
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
|
||||
self._initialized = False
|
||||
|
||||
self.parent = parent
|
||||
self.make_response = parent.make_response
|
||||
self._last_smtp_response = (None, None)
|
||||
self.tls = kwargs.pop('tls', False)
|
||||
self.ssl = kwargs.pop('ssl', False)
|
||||
self.debug = kwargs.pop('debug', 0)
|
||||
self.set_debuglevel(self.debug)
|
||||
self.user = kwargs.pop('user', None)
|
||||
self.password = kwargs.pop('password', None)
|
||||
|
||||
SMTP.__init__(self, **kwargs)
|
||||
|
||||
self.initialize()
|
||||
|
||||
def initialize(self):
|
||||
if self.tls:
|
||||
self.starttls()
|
||||
if self.user:
|
||||
self.login(user=self.user, password=self.password)
|
||||
self.ehlo_or_helo_if_needed()
|
||||
return self
|
||||
if not self._initialized:
|
||||
self.set_debuglevel(self.debug)
|
||||
if self.tls:
|
||||
self.starttls()
|
||||
if self.user:
|
||||
self.login(user=self.user, password=self.password)
|
||||
self.ehlo_or_helo_if_needed()
|
||||
self.initialized = True
|
||||
|
||||
def quit(self):
|
||||
"""Closes the connection to the email server."""
|
||||
try:
|
||||
SMTP.quit(self)
|
||||
except (smtplib.SMTPServerDisconnected, ):
|
||||
# This happens when calling quit() on a TLS connection
|
||||
# sometimes, or when the connection was already disconnected
|
||||
# by the server.
|
||||
self.close()
|
||||
|
||||
def data(self, msg):
|
||||
(code, msg) = SMTP.data(self, msg)
|
||||
self._last_smtp_response = (code, msg)
|
||||
return code, msg
|
||||
def _rset(self):
|
||||
try:
|
||||
self.rset()
|
||||
except smtplib.SMTPServerDisconnected:
|
||||
pass
|
||||
|
||||
def _send_one_mail(self, from_addr, to_addr, msg, mail_options=[], rcpt_options=[]):
|
||||
def sendmail(self, from_addr, to_addrs, msg, mail_options=None, rcpt_options=None):
|
||||
|
||||
if not to_addrs:
|
||||
return None
|
||||
|
||||
rcpt_options = rcpt_options or []
|
||||
mail_options = mail_options or []
|
||||
esmtp_opts = []
|
||||
if self.does_esmtp:
|
||||
if self.has_extn('size'):
|
||||
|
@ -102,48 +74,42 @@ class SMTPClientWithResponse(SMTP):
|
|||
response.set_status('mail', code, resp)
|
||||
|
||||
if code != 250:
|
||||
self.rset()
|
||||
self._rset()
|
||||
exc = smtplib.SMTPSenderRefused(code, resp, from_addr)
|
||||
response.set_exception(exc)
|
||||
return response
|
||||
|
||||
response.to_addr = to_addr
|
||||
if not isinstance(to_addrs, (list, tuple)):
|
||||
to_addrs = [to_addrs]
|
||||
|
||||
response.to_addrs = to_addrs
|
||||
response.rcpt_options = rcpt_options[:]
|
||||
response.refused_recipients = {}
|
||||
|
||||
(code, resp) = self.rcpt(to_addr, rcpt_options)
|
||||
response.set_status('rcpt', code, resp)
|
||||
for a in to_addrs:
|
||||
(code, resp) = self.rcpt(a, rcpt_options)
|
||||
response.set_status('rcpt', code, resp, recipient=a)
|
||||
if (code != 250) and (code != 251):
|
||||
response.refused_recipients[a] = (code, resp)
|
||||
|
||||
if (code != 250) and (code != 251):
|
||||
self.rset()
|
||||
exc = smtplib.SMTPRecipientsRefused(to_addr)
|
||||
if len(response.refused_recipients) == len(to_addrs):
|
||||
# the server refused all our recipients
|
||||
self._rset()
|
||||
exc = smtplib.SMTPRecipientsRefused(response.refused_recipient)
|
||||
response.set_exception(exc)
|
||||
return response
|
||||
|
||||
(code, resp) = self.data(msg)
|
||||
response.set_status('data', code, resp)
|
||||
if code != 250:
|
||||
self.rset()
|
||||
self._rset()
|
||||
exc = smtplib.SMTPDataError(code, resp)
|
||||
response.set_exception(exc)
|
||||
return response
|
||||
|
||||
response.success = True
|
||||
response._finished = True
|
||||
return response
|
||||
|
||||
def sendmail(self, from_addr, to_addrs, msg, mail_options=[], rcpt_options=[]):
|
||||
# Send one email and returns one response
|
||||
if not to_addrs:
|
||||
return []
|
||||
|
||||
assert isinstance(to_addrs, (list, tuple))
|
||||
|
||||
if len(to_addrs)>1:
|
||||
logger.warning('Beware: emails.smtp.client.SMTPClientWithResponse.sendmail sends full message to each email')
|
||||
|
||||
return [self._send_one_mail(from_addr, to_addr, msg, mail_options, rcpt_options) \
|
||||
for to_addr in to_addrs]
|
||||
|
||||
|
||||
|
||||
if _have_ssl:
|
||||
|
||||
|
@ -160,10 +126,11 @@ if _have_ssl:
|
|||
SMTP_SSL.__init__(self, **args)
|
||||
SMTPClientWithResponse.__init__(self, **kw)
|
||||
|
||||
def data(self, msg):
|
||||
(code, msg) = SMTP.data(self, msg)
|
||||
self._last_smtp_response = (code, msg)
|
||||
return code, msg
|
||||
def _rset(self):
|
||||
try:
|
||||
self.rset()
|
||||
except (ssl.SSLError, smtplib.SMTPServerDisconnected):
|
||||
pass
|
||||
|
||||
def quit(self):
|
||||
"""Closes the connection to the email server."""
|
||||
|
|
|
@ -365,17 +365,10 @@ class MessageSendMixin(object):
|
|||
if not from_addr:
|
||||
raise ValueError('No "from" addr')
|
||||
|
||||
params = dict(from_addr=from_addr,
|
||||
to_addrs=[to_addr, ],
|
||||
msg=self)
|
||||
if smtp_mail_options:
|
||||
params['mail_options'] = smtp_mail_options
|
||||
params = dict(from_addr=from_addr, to_addrs=[to_addr, ], msg=self,
|
||||
mail_options=smtp_mail_options, rcpt_options=smtp_rcpt_options)
|
||||
|
||||
if smtp_rcpt_options:
|
||||
params['rcpt_options'] = smtp_rcpt_options
|
||||
|
||||
response = smtp.sendmail(**params)
|
||||
return response[0]
|
||||
return smtp.sendmail(**params)
|
||||
|
||||
|
||||
class MessageTransformerMixin(object):
|
||||
|
|
|
@ -10,44 +10,46 @@ from emails.backend.smtp import SMTPBackend
|
|||
TRAVIS_CI = os.environ.get('TRAVIS')
|
||||
HAS_INTERNET_CONNECTION = not TRAVIS_CI
|
||||
|
||||
|
||||
def test_send_to_unknow_host():
|
||||
server = SMTPBackend(host='invalid-server.invalid-domain-42.com', port=25)
|
||||
response = server.sendmail(to_addrs='s@lavr.me', from_addr='s@lavr.me', msg='...')[0]
|
||||
server.close()
|
||||
assert response.status_code is None
|
||||
assert response.error is not None
|
||||
assert isinstance(response.error, IOError)
|
||||
print("response.error.errno=", response.error.errno)
|
||||
if HAS_INTERNET_CONNECTION:
|
||||
# IOError: [Errno 8] nodename nor servname provided, or not known
|
||||
assert response.error.errno==8
|
||||
|
||||
|
||||
SAMPLE_MESSAGE = {'html': '<p>Test from python-emails',
|
||||
'mail_from': 's@lavr.me',
|
||||
'mail_to': 'sergei-nko@yandex.ru',
|
||||
'subject': 'Test from python-emails'}
|
||||
|
||||
|
||||
def test_send_to_unknown_host():
|
||||
server = SMTPBackend(host='invalid-server.invalid-domain-42.com', port=25)
|
||||
response = server.sendmail(to_addrs='s@lavr.me', from_addr='s@lavr.me', msg=emails.html(**SAMPLE_MESSAGE))
|
||||
server.close()
|
||||
assert response.status_code is None
|
||||
assert response.error is not None
|
||||
assert isinstance(response.error, IOError)
|
||||
assert not response.success
|
||||
print("response.error.errno=", response.error.errno)
|
||||
if HAS_INTERNET_CONNECTION:
|
||||
# IOError: [Errno 8] nodename nor servname provided, or not known
|
||||
assert response.error.errno == 8
|
||||
|
||||
|
||||
def test_smtp_reconnect(smtp_server):
|
||||
|
||||
# Simulate server disconnection
|
||||
# Check that SMTPBackend will reconnect
|
||||
|
||||
server = SMTPBackend(host=smtp_server.host, port=smtp_server.port, debug=1)
|
||||
server.open()
|
||||
client = server.get_client()
|
||||
logging.debug('simulate socket disconnect')
|
||||
server.connection.sock.close() # simulate disconnect
|
||||
client.sock.close() # simulate disconnect
|
||||
response = server.sendmail(to_addrs='s@lavr.me',
|
||||
from_addr='s@lavr.me',
|
||||
msg=emails.html(**SAMPLE_MESSAGE))
|
||||
server.close()
|
||||
assert response.success
|
||||
print(response)
|
||||
|
||||
|
||||
def test_smtp_init_error(smtp_server):
|
||||
|
||||
# test error when ssl and tls arguments both set
|
||||
with pytest.raises(ValueError):
|
||||
SMTPBackend(host=smtp_server.host,
|
||||
port=smtp_server.port,
|
||||
|
@ -67,12 +69,14 @@ def test_smtp_dict1(smtp_server):
|
|||
response = emails.html(**SAMPLE_MESSAGE).send(smtp=smtp_server.as_dict())
|
||||
print(response)
|
||||
assert response.status_code == 250
|
||||
assert response.success
|
||||
|
||||
|
||||
def test_smtp_dict2(smtp_server_with_auth):
|
||||
response = emails.html(**SAMPLE_MESSAGE).send(smtp=smtp_server_with_auth.as_dict())
|
||||
print(response)
|
||||
assert response.status_code == 250
|
||||
assert response.success
|
||||
|
||||
def test_smtp_dict2(smtp_server_with_ssl):
|
||||
smtp = smtp_server_with_ssl.as_dict()
|
||||
|
@ -80,4 +84,5 @@ def test_smtp_dict2(smtp_server_with_ssl):
|
|||
response = message.send(smtp=smtp)
|
||||
print(response)
|
||||
assert response.status_code == 250
|
||||
message.smtp_pool[smtp].connection.quit()
|
||||
assert response.success
|
||||
message.smtp_pool[smtp].get_client().quit()
|
||||
|
|
Reference in New Issue