Add sendmail datamanager
issue5431 review17981002
This commit is contained in:
1 parent
576a132a76
commit
ba1faa1a76
7 files changed
+259
-26
No files matched your search
@@ -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
|
||||
|
||||
@@ -14,3 +14,4 @@ API Reference
|
||||
tools/singleton
|
||||
pool
|
||||
rpc
|
||||
sendmail
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 = []
|
||||
@@ -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
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user