Permalink
Browse files

Merge trunk.

  • Loading branch information...
2 parents 8244adc + 3f6ebd4 commit 0f6af12197300eed4d52036bd84c76be0038f467 @tseaver tseaver committed Jan 28, 2013
Showing with 202 additions and 58 deletions.
  1. +15 −2 CHANGES.txt
  2. +12 −2 docs/conf.py
  3. +38 −19 docs/index.rst
  4. +1 −1 pyramid_mailer/__init__.py
  5. +7 −0 pyramid_mailer/mailer.py
  6. +6 −2 pyramid_mailer/message.py
  7. +36 −15 pyramid_mailer/response.py
  8. +58 −7 pyramid_mailer/tests.py
  9. +7 −0 setup.cfg
  10. +19 −1 setup.py
  11. +3 −9 tox.ini
View
@@ -1,8 +1,21 @@
-After 0.9 (unreleased)
-----------------------
+After 0.10 (unreleased)
+-----------------------
- Dropped support for Python 2.5.
+0.10 (2012-11-22)
+-----------------
+
+- Set default transfer encoding for attachments to ``base64`` and allow
+ an optional ``transfer_encoding`` argument for attachments. This currently
+ supports ``base64`` or ``quoted-printable``.
+
+- Properly handle ``Mailer.from_settings`` boolean options including ``tls``
+ and ``ssl``.
+
+- Support ``setup.py dev`` (installs testing dependencies).
+
+- Use ``setup.py dev`` in tox.ini.
0.9 (2012-05-03)
----------------
View
@@ -57,7 +57,17 @@ def nothing(*arg):
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest']
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.viewcode',
+]
+
+intersphinx_mapping = {
+ 'pyramid': (
+ 'http://docs.pylonsproject.org/projects/pyramid/en/latest/',
+ None),
+}
# Add any paths that contain templates here, relative to this directory.
#templates_path = ['_templates']
@@ -76,7 +86,7 @@ def nothing(*arg):
# other places throughout the built documents.
#
# The short X.Y version.
-version = '0.9'
+version = '0.10'
# The full version, including alpha/beta/rc tags.
release = version
View
@@ -41,19 +41,19 @@ queries there.
Getting Started (The Easier Way)
--------------------------------
-In your application's configuration stanza (where you create a Pyramid
-"Configurator"), use the ``config.include`` method::
+In your application's configuration stanza use the
+:meth:`pyramid.config.Configurator.include` method::
config.include('pyramid_mailer')
-Thereafter in view code, use the ``pyramid_mailer.get_mailer`` API to obtain
-the configured mailer::
+Thereafter in view code, use the :func:`~pyramid_mailer.get_mailer` API to
+obtain the configured mailer::
from pyramid_mailer import get_mailer
mailer = get_mailer(request)
To send a message, you must first create a
-:class:`pyramid_mailer.message.Message` instance::
+:class:`~pyramid_mailer.message.Message` instance::
from pyramid_mailer.message import Message
@@ -79,11 +79,11 @@ otherwise provided.
If you don't want to use transactions, you can side-step them by using
-**send_immediately**::
+:meth:`~pyramid_mailer.mailer.Mailer.send_immediately`::
mailer.send_immediately(message, fail_silently=False)
-This will send the email immediately, outwith the transaction, so if it fails
+This will send the email immediately, without the transaction, so if it fails
you have to deal with it manually. The ``fail_silently`` flag will swallow
any connection errors silently - if it's not important whether the email gets
sent.
@@ -98,7 +98,7 @@ instance of :class:`pyramid_mailer.mailer.Mailer`::
mailer = Mailer()
-The ``Mailer`` class can take a number of optional settings, detailed in
+The mailer can take a number of optional settings, detailed in
:ref:`configuration`. It's a good idea to create a single ``Mailer`` instance
for your application, and add it to your registry in your configuration
setup::
@@ -124,16 +124,16 @@ construct and set your own mailer in this way.
Configuration
-------------
-If you configure a ``Mailer`` using
-:meth:`pyramid_mailer.mailer.Mailer.from_settings` or
+If you configure a :class:`~pyramid_mailer.mailer.Mailer` using
+:meth:`~pyramid_mailer.mailer.Mailer.from_settings` or via
``config.include('pyramid_mailer')``, you can pass the settings from your
Paste ``.ini`` file. For example::
[app:myproject]
mail.host = localhost
mail.port = 25
-By default, the prefix for is assumed to be `mail.`. If you use the
+By default, the prefix is assumed to be `mail.`. If you use the
``config.include`` mechanism, to set another prefix, use the
``pyramid_mailer.prefix`` key in the config file. For example::
@@ -142,7 +142,7 @@ By default, the prefix for is assumed to be `mail.`. If you use the
foo.port = 25
pyramid_mailer.prefix = foo.
-If you use the :meth:`pyramid_mailer.Mailer.Mailer.from_settings` or
+If you use the :meth:`pyramid_mailer.mailer.Mailer.from_settings` or
:func:`pyramid_mailer.mailer_factory_from_settings` API, these accept a
prefix directly; for example::
@@ -244,7 +244,9 @@ could be rewritten::
message.attach(attachment)
-
+A transfer encoding can be specified via the ``transfer_encoding`` option.
+Supported options are currently ``base64`` (the default) and
+``quoted-printable``.
Unit tests
----------
@@ -283,18 +285,19 @@ The ``DummyMailer`` instance keeps track of emails "sent" in two properties:
sent via :meth:`pyramid_mailer.mailer.Mailer.send`. Each stores the
individual ``Message`` instances::
- self.assertTrue(len(mailer.outbox) == 1)
- self.assertTrue(mailer.outbox[0].subject == "hello world")
+ self.assertEqual(len(mailer.outbox), 1)
+ self.assertEqual(mailer.outbox[0].subject, "hello world")
- self.assertTrue(len(mailer.queue) == 1)
- self.assertTrue(mailer.queue[0].subject == "hello world")
+ self.assertEqual(len(mailer.queue), 1)
+ self.assertEqual(mailer.queue[0].subject, "hello world")
Queue
-----
When you send mail to a queue via
-:meth:`pyramid_mailer.Mailer.send_to_queue`, the mail will be placed into a
-``maildir`` directory specified by the ``queue_path`` parameter or setting to
+:meth:`pyramid_mailer.mailer.Mailer.send_to_queue`, the mail will be placed
+into a ``maildir`` directory specified by the ``queue_path`` parameter or
+setting to
:class:`pyramid_mailer.mailer.Mailer`. A separate process will need to be
launched to monitor this maildir and take actions based on its state. Such a
program comes as part of `repoze_sendmail`_ (a dependency of the
@@ -312,6 +315,22 @@ the queue over time. ``qp`` has other options that allow you to choose
different settings. Use it's ``--help`` parameter to see more::
$ bin/qp --help
+
+.. note::
+
+ Sending messages via the queue requires the use of a transaction manager.
+ If no manager is enabled, it must be emulated by issuing a manual commit
+ via ``transaction.commit()``.
+
+ .. code-block:: python
+
+ import transaction
+ tx = transaction.begin()
+ mailer.send_to_queue(msg)
+ try:
+ tx.commit()
+ except Exception:
+ # handle a failed delivery
API
---
@@ -4,7 +4,7 @@
def mailer_factory_from_settings(settings, prefix='mail.'):
"""
Factory function to create a Mailer instance from settings.
- Equivalent to **Mailer.from_settings**
+ Equivalent to :meth:`pyramid_mailer.mailer.Mailer.from_settings`.
:versionadded: 0.2.2
"""
View
@@ -4,6 +4,8 @@
from repoze.sendmail.delivery import DirectMailDelivery
from repoze.sendmail.delivery import QueuedMailDelivery
+from pyramid.settings import asbool
+
class DummyMailer(object):
"""
@@ -165,6 +167,11 @@ def from_settings(cls, settings, prefix='mail.'):
kwargs = dict(((k[size:], settings[k]) for k in settings.keys() if
k in kwarg_names))
+ for key in ('tls', 'ssl'):
+ val = kwargs.get(key)
+ if val:
+ kwargs[key] = asbool(val)
+
return cls(**kwargs)
def send(self, message):
@@ -19,17 +19,20 @@ class Attachment(object):
:param content_type: file mimetype
:param data: the raw file data, either as string or file obj
:param disposition: content-disposition (if any)
+ :param transfer_encoding: content-transfer-encoding (if any)
"""
def __init__(self,
filename=None,
content_type=None,
data=None,
- disposition=None):
+ disposition=None,
+ transfer_encoding=None):
self.filename = filename
self.content_type = content_type
self.disposition = disposition or 'attachment'
+ self.transfer_encoding = transfer_encoding or 'base64'
self._data = data
@property
@@ -110,7 +113,8 @@ def get_response(self):
response.attach(attachment.filename,
attachment.content_type,
attachment.data,
- attachment.disposition)
+ attachment.disposition,
+ attachment.transfer_encoding)
response.update(self.extra_headers)
View
@@ -33,7 +33,9 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
+import base64
import os
+import quopri
import sys
import mimetypes
import string
@@ -60,7 +62,7 @@ def __init__(self, items=()):
self.body = None
self.content_encoding = {'Content-Type': (None, {}),
'Content-Disposition': (None, {}),
- 'Content-Transfer-Encoding': (None, {})}
+ 'Content-Transfer-Encoding': None}
def __getitem__(self, key):
return self.headers.get(normalize_header(key), None)
@@ -88,7 +90,8 @@ def keys(self):
"""Returns the sorted keys."""
return sorted(self.headers.keys())
- def attach_file(self, filename, data, ctype, disposition):
+ def attach_file(self, filename, data, ctype, disposition,
+ transfer_encoding=None):
"""
A file attachment is a raw attachment with a disposition that
indicates the file name.
@@ -101,6 +104,7 @@ def attach_file(self, filename, data, ctype, disposition):
part.content_encoding['Content-Type'] = (ctype, {'name': filename})
part.content_encoding['Content-Disposition'] = (disposition,
{'filename': filename})
+ part.content_encoding['Content-Transfer-Encoding'] = transfer_encoding
self.parts.append(part)
@@ -159,12 +163,13 @@ def __delitem__(self, name):
del self.base[name]
def attach(self, filename=None, content_type=None, data=None,
- disposition=None):
+ disposition=None, transfer_encoding=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.
+ give the data/content_type/filename/disposition/transfer-encoding
+ 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
@@ -189,7 +194,9 @@ def attach(self, filename=None, content_type=None, data=None,
self.attachments.append({'filename': filename,
'content_type': content_type,
'data': data,
- 'disposition': disposition,})
+ 'disposition': disposition,
+ 'transfer_encoding': transfer_encoding,})
+
def attach_part(self, part):
"""
Attaches a raw MailBase part from a MailRequest (or anywhere)
@@ -201,6 +208,7 @@ def attach_part(self, part):
'content_type': None,
'data': None,
'disposition': None,
+ 'transfer_encoding': None,
'part': part,
})
@@ -239,7 +247,8 @@ def __str__(self):
return self.to_message().as_string()
def _encode_attachment(self, filename=None, content_type=None, data=None,
- disposition=None, part=None):
+ disposition=None, transfer_encoding=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.
@@ -248,12 +257,14 @@ def _encode_attachment(self, filename=None, content_type=None, data=None,
self.base.parts.append(part)
elif filename:
if not data:
- f = open(filename)
+ # should be opened with binary mode to encode the data later
+ f = open(filename, mode='rb')
data = f.read()
f.close()
self.base.attach_file(filename, data, content_type,
- disposition or 'attachment')
+ disposition or 'attachment',
+ transfer_encoding or 'base64')
else:
self.base.attach_text(data, content_type)
@@ -371,18 +382,20 @@ def extract_payload(self, mail):
ctype, ctype_params = mail.content_encoding['Content-Type']
cdisp, cdisp_params = mail.content_encoding['Content-Disposition']
+ ctenc = mail.content_encoding.get('Content-Transfer-Encoding')
assert ctype, ("Extract payload requires that mail.content_encoding "
"have a valid Content-Type.")
- if ctype.startswith("text/"):
- self.set_payload(mail.body)
- else:
- if cdisp:
- # replicate the content-disposition settings
- self.add_header('Content-Disposition', cdisp, **cdisp_params)
+ if cdisp:
+ # replicate the content-disposition settings
+ self.add_header('Content-Disposition', cdisp, **cdisp_params)
+ if ctenc:
+ # need to encode because repoze.sendmail don't handle attachments
+ mail.body = encode_string(ctenc, mail.body)
+ self.add_header('Content-Transfer-Encoding', ctenc)
- self.set_payload(mail.body)
+ self.set_payload(mail.body)
def __repr__(self):
return "<MIMEPart '%s/%s': '%s', %r, multipart=%r>" % (
@@ -398,6 +411,14 @@ def is_nonstr_iter(v): # pragma: no cover
return False
return hasattr(v, '__iter__')
+def encode_string(encoding, data):
+ encoded = data
+ if encoding == 'base64':
+ encoded = base64.encodestring(data)
+ elif encoding == 'quoted-printable':
+ encoded = quopri.encodestring(data)
+ return encoded
+
# BBB Python 2 vs 3 compat
if sys.version < '3':
def is_nonstr_iter(v):
Oops, something went wrong.

0 comments on commit 0f6af12

Please sign in to comment.