Skip to content

Loading…

Feature: LoggingMailer mailer #8

Closed
wants to merge 3 commits into from

1 participant

@anler

Hi, please, forgive me if this is not the way of asking you if you are interested in merging a feature I've added to the repository.
The feature is a LoggingMailer that all it does is log the messages to the logger "pyramid_mailer".
Also I've changed the mailer_factory_from_settings() function in order to make possible decide which mailer to use from the .ini config file and not from the code, in this case I've copied a portion of pyramid_mailer.mailer.Mailer.from_settings() class method but left the original intact so tests that depends on it doesn't fail.
Last, I've opened an issue regarding the pyramid_mailer.response.MIMEPart.add_text() method because in my machine there is a failing test, so I've also done a little dirty workaround in this method to get the test pass.
Thanks

anler added some commits
@anler anler [FIX] UnicodeDecodeError in response.MIMEPart.add_text()
 fix to the UnicodeDecodeError in response.MIMEPart.add_text() method
9a3f9da
@anler anler Mailer class can be set in config file
In case you want to use different mailers in each environment
(development, production), you can specify which one through:
mail.mailer = MailerClassname
ac4a21b
@anler anler LoggingMailer feature 82ed458
@anler anler closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 10, 2011
  1. @anler

    [FIX] UnicodeDecodeError in response.MIMEPart.add_text()

    anler committed
     fix to the UnicodeDecodeError in response.MIMEPart.add_text() method
  2. @anler

    Mailer class can be set in config file

    anler committed
    In case you want to use different mailers in each environment
    (development, production), you can specify which one through:
    mail.mailer = MailerClassname
  3. @anler

    LoggingMailer feature

    anler committed
This page is out of date. Refresh to see the latest.
Showing with 282 additions and 9 deletions.
  1. +50 −0 README.txt
  2. +30 −2 pyramid_mailer/__init__.py
  3. +53 −1 pyramid_mailer/mailer.py
  4. +18 −6 pyramid_mailer/response.py
  5. +131 −0 pyramid_mailer/tests.py
View
50 README.txt
@@ -5,3 +5,53 @@ http://docs.pylonsproject.org/projects/pyramid_mailer/en/latest/ for more info.
pyramid_mailer uses code from the Lamson Project (http://lamsonproject.org/)
with permission. See the LICENSE.txt file for more information.
+
+IMPORTANT: This version has been modified by me in order to get one
+features I need in my projects (since I come from a Django background).
+
+The changes are:
+
+1. Now you can specify the mailer type (Mailer, DummyMailer, etc) from
+ your project .ini file, for example, in my development.ini file I have:
+
+ mail.mailer = pyramid_mailer.mailer.LoggingMailer
+
+ Here I'm telling that I want my mailers to be instances of
+ pyramid_mailer.mailer.LoggingMailer
+
+2. Added LoggingMailer class in pyramid_mailer.mailer module. This mailer
+ all it does is log all the messages you send using any of the
+ standard mailer methods: send, send_immediately, send_to_queue
+ By default the logger used by this class is named `pyramid_mailer` but
+ you can change it through the .ini file.
+ For example, let's configure the LoggingMailer to send all the messages
+ to the console:
+ (in my case, I write this in development.ini)
+
+ [app:main]
+ ...
+ mail.mailer = pyramid_mailer.mailer.LoggingMailer
+ ...
+ [loggers]
+ keys = root, ..., pyramid_mailer
+ ...
+ [logger_pyramid_mailer]
+ level = INFO
+ handlers = console
+ qualname = pyramid_mailer
+ ...
+
+ With that in place, any time you send a message from your views, the
+ message will be logged to the console and to the logging of the debug
+ toolbar in case you are using it.
+ An example of using this mailer in the views:
+
+ from pyramid_mailer import get_mailer
+ from pyramid_mailer.message import Message
+
+ def myview(request):
+ message = Message(...)
+ mailer = get_mailer(request)
+ mailer.send(message) # The message is logged
+ return {}
+
View
32 pyramid_mailer/__init__.py
@@ -1,4 +1,4 @@
-from pyramid_mailer.mailer import Mailer
+from pyramid_mailer import mailer
from pyramid_mailer.interfaces import IMailer
def mailer_factory_from_settings(settings, prefix='mail.'):
@@ -8,7 +8,35 @@ def mailer_factory_from_settings(settings, prefix='mail.'):
:versionadded: 0.2.2
"""
- return Mailer.from_settings(settings, prefix)
+ import importlib
+
+ mailer_import_path = settings.pop(prefix + 'mailer',
+ 'pyramid_mailer.mailer.Mailer')
+ import_parts = mailer_import_path.split('.')
+ mailer_classname = import_parts.pop(-1)
+ import_path = '.'.join(import_parts)
+
+ try:
+ module = importlib.import_module(import_path)
+ Mailer = getattr(module, mailer_classname)
+ except (ImportError, ValueError):
+ raise ImportError("Can't import module `%s`" % (import_path))
+ except AttributeError:
+ raise NameError("Invalid value for `mail.mailer` in config")
+
+ settings = settings or {}
+
+ kwarg_names = [prefix + k for k in (
+ 'host', 'port', 'username',
+ 'password', 'tls', 'ssl', 'keyfile',
+ 'certfile', 'queue_path', 'debug', 'default_sender')]
+
+ size = len(prefix)
+
+ kwargs = dict(((k[size:], settings[k]) for k in settings.keys() if
+ k in kwarg_names))
+
+ return Mailer(**kwargs)
def includeme(config):
View
54 pyramid_mailer/mailer.py
@@ -1,3 +1,4 @@
+import logging
import smtplib
from repoze.sendmail.mailer import SMTPMailer
@@ -5,6 +6,57 @@
from repoze.sendmail.delivery import QueuedMailDelivery
+class LoggingMailer(object):
+ """
+ A dummy mailing instance that writes messages to a log instead of sending
+ them.
+
+ By default, the logger name used is `pyramid_mailer` (but is configurable
+ throught the option `mail.logger_name`), with it, it's up to you to
+ configure the logging as you want. A brief example could be:
+ (in file development.ini)
+ ...
+ [loggers]
+ keys = root, sqalchemy, pyramid_mailer
+ ...
+ [logger_pyramid_mailer]
+ level = INFO
+ handlers = console
+ qualname = pyramid_mailer
+ ...
+ """
+ logger_name = 'pyramid_mailer'
+
+ def __init__(self, *args, **kwargs):
+ logger_name = kwargs.pop('logger_name', self.logger_name)
+ self.logger = logging.getLogger(logger_name)
+
+ def send(self, message):
+ """
+ Logs sending a transactional message.
+
+ :param message: a **Message** instance.
+ """
+ self.logger.info(message.to_message())
+
+ def send_immediately(self, message, fail_silently=False):
+ """
+ Logs sending an immediate (non-transactional) message.
+
+ :param message: a **Message** instance.
+ :param fail_silently: swallow connection errors (ignored here)
+ """
+ self.send(message)
+
+ def send_to_queue(self, message):
+ """
+ Logs sending to a maildir queue.
+
+ :param message: a **Message** instance.
+ """
+ self.send(message)
+
+
class DummyMailer(object):
"""
Dummy mailing instance, used for example in unit tests.
@@ -13,7 +65,7 @@ class DummyMailer(object):
Queued messages are instead added to **queue** property.
"""
- def __init__(self):
+ def __init__(self, *args, **kwargs):
self.outbox = []
self.queue = []
View
24 pyramid_mailer/response.py
@@ -34,6 +34,7 @@
# POSSIBILITY OF SUCH DAMAGE.
import os
+import sys
import mimetypes
import string
from email import encoders
@@ -365,12 +366,23 @@ def __init__(self, type, **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'
+ charset = 'ascii'
+ if not isinstance(content, str):
+ # Python 3 str or Python 2 unicode
+ try:
+ encoded = content.encode(charset)
+ except UnicodeError:
+ encoded = content.encode('utf-8')
+ charset = 'utf-8'
+ else:
+ # Python 2 str (already encoded)
+ encoded = content
+ try:
+ # Is the string encoded in ascii?
+ content.decode(charset)
+ except UnicodeError:
+ # If is not, assume utf-8
+ charset = 'utf-8'
self.set_payload(encoded, charset=charset)
View
131 pyramid_mailer/tests.py
@@ -271,6 +271,102 @@ def test_is_bad_headers_if_bad_headers(self):
self.assert_(msg.is_bad_headers())
+
+class TestLoggingMailer(unittest.TestCase):
+
+ def _get_handler(self):
+ import logging
+
+ class MockLoggingHandler(logging.Handler):
+ "Mock logging handler to check for expected logs."
+ def emit(self, record):
+ self.last_log_levelname = record.levelname.lower()
+ self.last_log_message = record.getMessage()
+
+ return MockLoggingHandler(level=logging.INFO)
+
+
+ def test_dummy_send(self):
+ import logging
+ from pyramid_mailer.mailer import LoggingMailer
+ from pyramid_mailer.message import Message
+
+ mailer = LoggingMailer()
+ mailer.logger.setLevel(logging.INFO)
+ handler = self._get_handler()
+ mailer.logger.addHandler(handler)
+
+ msg = Message(subject="testing",
+ sender="sender@example.com",
+ recipients=["tester@example.com"],
+ body="test")
+
+ mailer.send(msg)
+
+ self.assertEqual('info', handler.last_log_levelname)
+ self.assertEqual(str(msg.to_message()), handler.last_log_message)
+
+ def test_dummy_send_immediately(self):
+ import logging
+ from pyramid_mailer.mailer import LoggingMailer
+ from pyramid_mailer.message import Message
+
+ mailer = LoggingMailer()
+ mailer.logger.setLevel(logging.INFO)
+ handler = self._get_handler()
+ mailer.logger.addHandler(handler)
+
+ msg = Message(subject="testing",
+ sender="sender@example.com",
+ recipients=["tester@example.com"],
+ body="test")
+
+ mailer.send_immediately(msg)
+
+ self.assertEqual('info', handler.last_log_levelname)
+ self.assertEqual(str(msg.to_message()), handler.last_log_message)
+
+ def test_dummy_send_immediately_and_fail_silently(self):
+ import logging
+ from pyramid_mailer.mailer import LoggingMailer
+ from pyramid_mailer.message import Message
+
+ mailer = LoggingMailer()
+ mailer.logger.setLevel(logging.INFO)
+ handler = self._get_handler()
+ mailer.logger.addHandler(handler)
+
+ msg = Message(subject="testing",
+ sender="sender@example.com",
+ recipients=["tester@example.com"],
+ body="test")
+
+ mailer.send_immediately(msg, True)
+
+ self.assertEqual('info', handler.last_log_levelname)
+ self.assertEqual(str(msg.to_message()), handler.last_log_message)
+
+ def test_dummy_send_to_queue(self):
+ import logging
+ from pyramid_mailer.mailer import LoggingMailer
+ from pyramid_mailer.message import Message
+
+ mailer = LoggingMailer()
+ mailer.logger.setLevel(logging.INFO)
+ handler = self._get_handler()
+ mailer.logger.addHandler(handler)
+
+ msg = Message(subject="testing",
+ sender="sender@example.com",
+ recipients=["tester@example.com"],
+ body="test")
+
+ mailer.send_to_queue(msg)
+
+ self.assertEqual('info', handler.last_log_levelname)
+ self.assertEqual(str(msg.to_message()), handler.last_log_message)
+
+
class TestMailer(unittest.TestCase):
def test_dummy_send_immediately(self):
@@ -458,6 +554,8 @@ def test_from_settings_factory(self):
except ImportError: # pragma: no cover
from smtplib import SMTP
ssl_enabled = False
+ from pyramid_mailer.mailer import (Mailer, DummyMailer, SMTP_SSLMailer,
+ SMTPMailer, LoggingMailer)
from pyramid_mailer import mailer_factory_from_settings
settings = {'mymail.host' : 'my.server.com',
@@ -488,6 +586,39 @@ def test_from_settings_factory(self):
self.assert_(mailer.queue_delivery.queuePath == '/tmp')
self.assert_(mailer.direct_delivery.mailer.debug_smtp == 1)
+ def test_mailer_class_from_settings_factory(self):
+ from pyramid_mailer.mailer import (Mailer, DummyMailer, SMTP_SSLMailer,
+ SMTPMailer, LoggingMailer)
+ from pyramid_mailer import mailer_factory_from_settings
+
+ settings = {'mymail.mailer': 'pyramid_mailer.mailer.Mailer'}
+ mailer = mailer_factory_from_settings(settings, prefix='mymail.')
+ self.assertIsInstance(mailer, Mailer)
+
+ settings = {'mymail.mailer': 'pyramid_mailer.mailer.DummyMailer'}
+ mailer = mailer_factory_from_settings(settings, prefix='mymail.')
+ self.assertIsInstance(mailer, DummyMailer)
+
+ settings = {'mymail.mailer': 'pyramid_mailer.mailer.SMTP_SSLMailer'}
+ mailer = mailer_factory_from_settings(settings, prefix='mymail.')
+ self.assertIsInstance(mailer, SMTP_SSLMailer)
+
+ settings = {'mymail.mailer': 'pyramid_mailer.mailer.SMTPMailer'}
+ mailer = mailer_factory_from_settings(settings, prefix='mymail.')
+ self.assertIsInstance(mailer, SMTPMailer)
+
+ settings = {'mymail.mailer': 'pyramid_mailer.mailer.LoggingMailer'}
+ mailer = mailer_factory_from_settings(settings, prefix='mymail.')
+ self.assertIsInstance(mailer, LoggingMailer)
+
+ settings = {'mymail.mailer': 'pyramid_mailer.mailer.InvalidMailer'}
+ self.assertRaises(NameError, mailer_factory_from_settings, settings,
+ prefix='mymail.')
+
+ settings = {'mymail.mailer': 'invalid.module'}
+ self.assertRaises(ImportError, mailer_factory_from_settings, settings,
+ prefix='mymail.')
+
def test_from_settings(self):
Something went wrong with that request. Please try again.