Add sendmail datamanager

issue5431
review17981002
This commit is contained in:
Cédric Krier committed 2016-03-28 22:40:37 +02:00
1 parent 576a132a76
commit ba1faa1a76
7 files changed
+259 -26

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
* Add sendmail module to send transactional email
* Support Two-Phase Commit in Transaction
* Allow Report to generate text plain, XML, HTML and XHTML
* Add workflow graph on ir.model
+1
View File
@@ -14,3 +14,4 @@ API Reference
tools/singleton
pool
rpc
sendmail
+44
View File
@@ -0,0 +1,44 @@
.. _ref-sendmail:
.. module:: trytond.sendmail
========
Sendmail
========
.. method:: sendmail_transactional(from_addr, to_addrs, msg[, transaction[, datamanager]])
Send email message only if the current transaction is successfully committed.
The required arguments are an `RFC 822`_ from-address string, a list of `RFC
822`_ to-address strings (a bare string will be treated as a list with 1
address), and an email message.
The caller may pass a :class:`Transaction` instance to join otherwise the
current one will be joined. A specific data manager can be specified otherwise
the default :class:`SMTPDataManager` will be used for sending email.
.. warning::
An SMTP failure will be only logged without raising any exception.
.. method:: sendmail(from_addr, to_addrs, msg[, server])
Send email message like :meth:`sendmail_transactional` but directly without
caring about the transaction.
The caller may pass a server instance from `smtplib`_.
.. method:: get_smtp_server([uri])
Return a SMTP instance from `smtplib`_ using the `uri` or the one defined in
the `email` section of the :ref:`configuration <topics-configuration>`.
.. class:: SMTPDataManager([uri])
A :class:`SMTPDataManager` implements a data manager which send queued email at
commit. An option optional `uri` can be passed to configure the SMTP connection.
.. method:: SMTPDataManager.put(from_addr, to_addrs, msg)
Queue the email message to send.
.. _`RFC 822`: https://tools.ietf.org/html/rfc822.html
.. _`smtplib`: https://docs.python.org/2/library/smtplib.html
+2 -8
View File
@@ -10,12 +10,12 @@ from email.header import Header
from ast import literal_eval
from ..model import ModelView, ModelSQL, fields, dualmethod
from ..tools import get_smtp_server
from ..transaction import Transaction
from ..pool import Pool
from .. import backend
from ..config import config
from ..cache import Cache
from ..sendmail import sendmail
__all__ = [
'Cron',
@@ -160,13 +160,7 @@ class Cron(ModelSQL, ModelView):
if not to_addr:
logger.error(msg.as_string())
else:
try:
server = get_smtp_server()
server.sendmail(from_addr, to_addr, msg.as_string())
server.quit()
except Exception:
logger.error('Unable to deliver email:\n %s',
msg.as_string(), exc_info=True)
sendmail(from_addr, to_addr, msg)
@dualmethod
@ModelView.button
+104
View File
@@ -0,0 +1,104 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import logging
import smtplib
import urllib
from email.message import Message
from .config import config, parse_uri
from .transaction import Transaction
__all__ = ['sendmail_transactional', 'sendmail', 'SMTPDataManager']
logger = logging.getLogger(__name__)
def sendmail_transactional(
from_addr, to_addrs, msg, transaction=None, datamanager=None):
if transaction is None:
transaction = Transaction()
assert isinstance(transaction, Transaction), transaction
if datamanager is None:
datamanager = SMTPDataManager()
datamanager = transaction.join(datamanager)
datamanager.put(from_addr, to_addrs, msg)
def sendmail(from_addr, to_addrs, msg, server=None):
if server is None:
server = get_smtp_server()
quit = True
else:
quit = False
try:
senderrs = server.sendmail(from_addr, to_addrs, msg.as_string())
except smtplib.SMTPException:
logger.error('fail to send email', exc_info=True)
if senderrs:
logger.warn('fail to send email to %s', senderrs)
if quit:
server.quit()
def get_smtp_server(uri=None):
if uri is None:
uri = config.get('email', 'uri')
uri = parse_uri(uri)
if uri.scheme.startswith('smtps'):
server = smtplib.SMTP_SSL(uri.hostname, uri.port)
else:
server = smtplib.SMTP(uri.hostname, uri.port)
if 'tls' in uri.scheme:
server.starttls()
if uri.username and uri.password:
server.login(
urllib.unquote_plus(uri.username),
urllib.unquote_plus(uri.password))
return server
class SMTPDataManager(object):
def __init__(self, uri=None):
self.uri = uri
self.queue = []
self._server = None
def put(self, from_addr, to_addrs, msg):
assert isinstance(msg, Message), msg
self.queue.append((from_addr, to_addrs, msg))
def __eq__(self, other):
if not isinstance(other, SMTPDataManager):
return NotImplemented
return self.uri == other.uri
def abort(self, trans):
self._finish()
def tpc_begin(self, trans):
pass
def commit(self, trans):
pass
def tpc_vote(self, trans):
if self._server is None:
self._server = get_smtp_server(self.uri)
def tpc_finish(self, trans):
if self._server is not None:
for from_addr, to_addrs, msg in self.queue:
sendmail(from_addr, to_addrs, msg, server=self._server)
self._server.quit()
self._finish()
def tpc_abort(self, trans):
if self._server:
self._server.close()
self._finish()
def _finish(self):
self._server = None
self.queue = []
+101
View File
@@ -0,0 +1,101 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import unittest
import smtplib
from email.message import Message
from mock import Mock, patch, call
from trytond.sendmail import (
sendmail_transactional, sendmail, SMTPDataManager, get_smtp_server)
from trytond.transaction import Transaction
from .test_tryton import with_transaction
class SendmailTestCase(unittest.TestCase):
'Test sendmail'
@with_transaction()
def test_sendmail_transactional(self):
'Test sendmail_transactional'
message = Mock()
datamanager = Mock()
sendmail_transactional(
'tryton@example.com', 'foo@example.com', message,
datamanager=datamanager)
datamanager.put.assert_called()
def test_sendmail(self):
'Test sendmail'
message = Mock()
server = Mock()
sendmail(
'tryton@example.com', 'foo@example.com', message, server=server)
server.sendmail.assert_called(
'tryton@example.com', 'foo@example.com', message)
server.quit.assert_not_called()
def test_get_smtp_server(self):
'Test get_smtp_server'
with patch.object(smtplib, 'SMTP') as SMTP:
SMTP.return_value = server = Mock()
self.assertEqual(get_smtp_server('smtp://localhost:25'), server)
SMTP.assert_called('localhost', 25)
with patch.object(smtplib, 'SMTP') as SMTP:
SMTP.return_value = server = Mock()
self.assertEqual(
get_smtp_server('smtp://foo:bar@localhost:25'), server)
SMTP.assert_called('localhost', 25)
server.login.assert_called('foo', 'bar')
with patch.object(smtplib, 'SMTP_SSL') as SMTP:
SMTP.return_value = server = Mock()
self.assertEqual(
get_smtp_server('smtps://localhost:25'), server)
SMTP.assert_called('localhost', 25)
with patch.object(smtplib, 'SMTP') as SMTP:
SMTP.return_value = server = Mock()
self.assertEqual(
get_smtp_server('smtp+tls://localhost:25'), server)
SMTP.assert_called('localhost', 25)
server.starttls.assert_called()
@patch('trytond.sendmail.get_smtp_server')
@with_transaction()
def test_SMTPDataManager(self, get_smtp_server):
'Test SMTPDataManager'
transaction = Transaction()
get_smtp_server.return_value = server = Mock()
datamanager = transaction.join(SMTPDataManager())
# multiple join must return the same
self.assertEqual(transaction.join(SMTPDataManager()), datamanager)
msg1 = Mock(Message)
msg2 = Mock(Message)
datamanager.put('foo@example.com', 'bar@example.com', msg1)
datamanager.put('bar@example.com', 'foo@example.com', msg2)
transaction.commit()
server.sendmail.assert_has_calls([
call('foo@example.com', 'bar@example.com', msg1.as_string()),
call('bar@example.com', 'foo@example.com', msg2.as_string()),
])
server.quit.assert_called_once_with()
self.assertFalse(datamanager.queue)
server.reset_mock()
datamanager.put('foo@example.com', 'bar@example.com', Mock(Message))
transaction.rollback()
server.sendmail.assert_not_called()
self.assertFalse(datamanager.queue)
def suite():
return unittest.TestLoader().loadTestsFromTestCase(SendmailTestCase)
+6 -18
View File
@@ -7,18 +7,16 @@ Miscelleanous tools used by tryton
import os
import sys
import subprocess
import smtplib
from array import array
from itertools import islice
import types
import urllib
import io
import warnings
from sql import Literal
from sql.operators import Or
from trytond.const import OPERATORS
from trytond.config import config, parse_uri
def find_in_path(name):
@@ -106,21 +104,11 @@ def get_smtp_server():
:return: A SMTP instance. The quit() method must be call when all
the calls to sendmail() have been made.
"""
uri = parse_uri(config.get('email', 'uri'))
if uri.scheme.startswith('smtps'):
smtp_server = smtplib.SMTP_SSL(uri.hostname, uri.port)
else:
smtp_server = smtplib.SMTP(uri.hostname, uri.port)
if 'tls' in uri.scheme:
smtp_server.starttls()
if uri.username and uri.password:
smtp_server.login(
urllib.unquote_plus(uri.username),
urllib.unquote_plus(uri.password))
return smtp_server
from ..sendmail import _get_smtp_server
warnings.warn(
'get_smtp_server is deprecated use trytond.sendmail',
DeprecationWarning)
return _get_smtp_server()
def memoize(maxsize):