From c98712916899305662859eb31168bee3866ce186 Mon Sep 17 00:00:00 2001
From: Sergey Lavrinenko
Date: Sat, 21 Feb 2015 23:56:59 +0300
Subject: [PATCH] python-emails refactored and redefined
---
.coveragerc | 8 +
.gitignore | 1 +
.travis.yml | 23 +-
Makefile | 10 +
README.rst | 151 ++++---
emails/__init__.py | 6 +-
emails/exc.py | 4 +
emails/loader/__init__.py | 117 ++++--
emails/loader/cssinliner.py | 170 --------
emails/loader/helpers.py | 50 +--
emails/loader/htmlloader.py | 392 ------------------
.../loader/{fileloader.py => local_store.py} | 116 +++++-
emails/loader/stylesheets.py | 125 ------
emails/loader/wrappers.py | 96 -----
emails/message.py | 251 +++++++----
emails/smtp/backend.py | 29 +-
emails/smtp/client.py | 48 ++-
emails/smtp/factory.py | 2 +-
emails/store/file.py | 56 ++-
emails/store/store.py | 55 ++-
emails/testsuite/conftest.py | 82 +++-
.../test_django_integrations.py | 12 +-
.../loader/data/html_import/oldornament.zip | Bin 96933 -> 88907 bytes
.../oldornament/html/left_sidebar.html | 152 -------
.../oldornament/html/right_sidebar.html | 153 -------
.../oldornament/{html => }/images/arrow.png | Bin
.../{html => }/images/banner-bottom.png | Bin
.../{html => }/images/banner-middle.gif | Bin
.../{html => }/images/banner-top.gif | Bin
.../oldornament/{html => }/images/bg-all.jpg | Bin
.../{html => }/images/bg-content.jpg | Bin
.../oldornament/{html => }/images/bg-main.jpg | Bin
.../oldornament/{html => }/images/divider.jpg | Bin
.../{html => }/images/divider2.jpg | Bin
.../{html => }/images/flourish.png | Bin
.../oldornament/{html => }/images/img01.jpg | Bin
.../oldornament/{html => }/images/img02.jpg | Bin
.../oldornament/{html => }/images/img03.jpg | Bin
.../oldornament/{html => }/images/spacer.gif | Bin
.../{html/full_width.html => index.html} | 2 +-
emails/testsuite/loader/test_loader.py | 208 ----------
emails/testsuite/loader/test_loaders.py | 106 +++++
emails/testsuite/message/helpers.py | 48 +--
emails/testsuite/message/test_dkim.py | 6 -
emails/testsuite/message/test_message.py | 14 +-
emails/testsuite/message/test_send.py | 87 ++--
.../testsuite/{loader => store}/test_store.py | 10 +-
emails/testsuite/test_readme.py | 14 +
emails/testsuite/test_utils.py | 11 +
.../testsuite/transformer/test_transformer.py | 42 ++
emails/transformer.py | 315 ++++++++++++++
emails/utils.py | 33 +-
requirements/base.txt | 1 +
requirements/tests-2.6.txt | 4 +-
requirements/tests-2.7.txt | 5 +-
requirements/tests-3.3.txt | 5 +-
requirements/tests-3.4.txt | 5 +-
requirements/tests-base.txt | 3 +
scripts/make_rfc822.py | 87 ++--
setup.py | 28 +-
tox.ini | 26 ++
61 files changed, 1374 insertions(+), 1795 deletions(-)
create mode 100644 .coveragerc
create mode 100644 Makefile
create mode 100644 emails/exc.py
delete mode 100644 emails/loader/cssinliner.py
delete mode 100644 emails/loader/htmlloader.py
rename emails/loader/{fileloader.py => local_store.py} (61%)
delete mode 100644 emails/loader/stylesheets.py
delete mode 100644 emails/loader/wrappers.py
rename emails/testsuite/{smtp => django_}/test_django_integrations.py (62%)
delete mode 100755 emails/testsuite/loader/data/html_import/oldornament/html/left_sidebar.html
delete mode 100755 emails/testsuite/loader/data/html_import/oldornament/html/right_sidebar.html
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/arrow.png (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/banner-bottom.png (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/banner-middle.gif (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/banner-top.gif (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/bg-all.jpg (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/bg-content.jpg (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/bg-main.jpg (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/divider.jpg (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/divider2.jpg (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/flourish.png (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/img01.jpg (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/img02.jpg (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/img03.jpg (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html => }/images/spacer.gif (100%)
rename emails/testsuite/loader/data/html_import/oldornament/{html/full_width.html => index.html} (99%)
delete mode 100644 emails/testsuite/loader/test_loader.py
create mode 100644 emails/testsuite/loader/test_loaders.py
rename emails/testsuite/{loader => store}/test_store.py (68%)
create mode 100644 emails/testsuite/test_readme.py
create mode 100644 emails/testsuite/test_utils.py
create mode 100644 emails/testsuite/transformer/test_transformer.py
create mode 100644 emails/transformer.py
create mode 100644 requirements/tests-base.txt
create mode 100644 tox.ini
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..2ac526f
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,8 @@
+[run]
+source = emails
+
+[report]
+omit =
+ emails/testsuite*
+ emails/packages*
+ emails/compat*
diff --git a/.gitignore b/.gitignore
index 9dc11a4..c84f441 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
local_settings.py
+local_*_settings.py
*.py[cod]
# C extensions
diff --git a/.travis.yml b/.travis.yml
index e8f3f4c..7104f03 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,15 +1,20 @@
language: python
+sudo: no
+
python:
- "2.6"
- "2.7"
- "3.3"
- "3.4"
-script: py.test
+script: py.test --cov emails
+
+before_install:
+ - travis_retry pip install coverage coveralls pytest-cov
install:
- - pip install -r requirements/tests-$TRAVIS_PYTHON_VERSION.txt --use-mirrors
+ - travis_retry pip install -r requirements/tests-$TRAVIS_PYTHON_VERSION.txt
env:
- PIP_DOWNLOAD_CACHE=$HOME/.pip-cache
@@ -17,3 +22,17 @@ env:
cache:
directories:
- $HOME/.pip-cache/
+
+after_success:
+ # Report coverage results to coveralls.io
+ - coveralls
+
+ deploy:
+ provider: pypi
+ user: lavr
+ password:
+ secure: "WuFOsmKW77foHa0Ywv7pwXNvSQ+lHSx/IlYxPTuE7dTj1mNgvXC48NXQONY1ZEDiysryimgfsqumvx6PqLsFmOkG4r9k3gaau0eHE063+/hse0YvbqpnzIWa1FTe4yxreJeEHWSiNyAyo0ERaZVMcnj1ii6paHzuMVuCQ/BwV3k="
+ on:
+ branch: master
+ tags: true
+ distributions: "sdist bdist_wheel"
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..60768f0
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,10 @@
+
+clean:
+ find . -name '*pyc' -exec rm -f {} \;
+ find . -name '*py~' -exec rm -f {} \;
+
+test:
+ tox
+
+pypi:
+ python setup.py sdist upload
diff --git a/README.rst b/README.rst
index 0fc7672..be8aab2 100644
--- a/README.rst
+++ b/README.rst
@@ -1,11 +1,23 @@
python-emails
=============
-Emails without pain for python.
+Modern email handling in python.
+
+Features
+--------
+
+- HTML-email message abstraction
+- Method to transform html body:
+
+ - css inlining (using peterbe's premailer)
+ - image inlining
+- DKIM signature
+- Message loaders
+- Send directly or via django email backend
-What can you do:
-----------------
+Examples:
+---------
Create message:
@@ -21,54 +33,106 @@ Attach files or inline images:
::
- message.attach( data=open('event.ics'), filename='Event.ics' )
- message.attach( data=open('image.png'), filename='image.png', content_disposition='inline' )
+ message.attach(data=open('event.ics'), filename='Event.ics')
+ message.attach(data=open('image.png'), filename='image.png',
+ content_disposition='inline')
-Add DKIM easily:
-
-::
-
- message.dkim( key=open('my.key'), domain='mycompany.com', selector='newsletter' )
-
-
-
-Templating:
+Use templates:
::
from emails.template import JinjaTemplate as T
- message = emails.html(subject=T('Payment Receipt No.{{no}}'),
- html=T('
Dear {{ name }}! This is a receipt for your subscription...'),
+ message = emails.html(subject=T('Payment Receipt No.{{ billno }}'),
+ html=T('
Dear {{ name }}! This is a receipt...'),
mail_from=('ABC', 'robot@mycompany.com'))
- message.send(to=('John Brown', 'jbrown@gmail.com'), render={'name': 'John Brown', 'billno':'141051906163'} )
+ message.send(to=('John Brown', 'jbrown@gmail.com'),
+ render={'name': 'John Brown', 'billno': '141051906163'})
-Send without pain and (even) get response:
+
+
+Add DKIM signature:
::
- SMTP = { 'host':'smtp.mycompany.com', 'port': 465, 'ssl': True }
- r = message.send(to=('John Brown', 'jbrown@gmail.com'), smtp=SMTP)
+ message.dkim(key=open('my.key'), domain='mycompany.com', selector='newsletter')
+
+Generate email.message or rfc822 string:
+
+::
+
+ m = message.as_message()
+ s = message.as_string()
+
+
+
+Send and get response from smtp server:
+
+::
+
+ r = message.send(to=('John Brown', 'jbrown@gmail.com'),
+ smtp={'host':'smtp.mycompany.com', 'port': 465, 'ssl': True})
assert r.status_code == 250
+Or send via Django email backend:
+
+::
+ from django.core.mail import get_connection
+ from emails.message import DjangoMessageProxy
+ c = django.core.mail.get_connection()
+ c.send_messages([DjangoMessageProxy(message), ])
+HTML transformer
+----------------
-One more thing
---------------
-Library ships with fairy email-from-html loader.
-Design email with less pain or even let designers make design:
+Message HTML body can be modified with 'transformer' object:
::
- import emails
- URL = 'http://_youproject_.github.io/newsletter/2013-08-14/index.html'
- page = emails.loader.from_url(URL, css_inline=True, make_links_absolute=True)
- message = emails.html(html=page.html, ...)
- for mail_to in _get_maillist():
- message.send(to=mail_to)
+ >>> message = emails.Message(html="")
+ >>> message.transformer.apply_to_images(func=lambda src, **kw: 'http://mycompany.tld/images/'+src)
+ >>> message.transformer.save()
+ >>> message.html
+ u'
'
+
+Code example to make images inline:
+
+::
+
+ >>> message = emails.Message(html="")
+ >>> message.attach(filename='promo.png', data=open('promo.png'))
+ >>> message.attachments['promo.png'].is_inline = True
+ >>> message.transformer.synchronize_inline_images()
+ >>> message.transformer.save()
+ >>> message.html
+ u''
+
+
+Loaders
+-------
+
+python-emails ships with couple of loaders.
+
+Load message from url:
+
+::
+
+ import emails.loader
+ message = emails.loader.from_url(url="http://xxx.github.io/newsletter/2015-08-14/index.html")
+
+
+Load from zipfile or directory:
+
+::
+
+ message = emails.loader.from_zipfile(open('design_pack.zip'))
+ message = emails.loader.from_directory('/home/user/design_pack')
+
+Zipfile and directory loaders require at least one html file (with "html" extension).
+
Install
-------
@@ -88,30 +152,18 @@ Install on Ubuntu from PPA:
$ [sudo] apt-get install python-emails
-Features
---------
-
-- Internationalization & Unicode bodies
-- DKIM signatures
-- HTML page loader & CSS inliner
-- Body and attachments http import
-- Body & headers preprocessors
TODO
----
-- Python3 (almost done)
-- Add "safety stuff" from django (done)
-- Django integration (django.core.mail.backends.smtp.EmailBackend subclass)
-- Flask extension
+
- Documentation
- 100% test coverage
- More accurate smtp session handling
-- Some patches for pydkim performance (i.e. preload key once, not each time)
-- More genius css inliner
-- Catch all bugs
- ESP integration: Amazon SES, SendGrid, ...
-- deb package (ubuntu package done)
+- deb package (ubuntu package almost done)
- rpm package
+- Patch pydkim for performance (i.e. preload key once, not each time)
+- Flask extension
How to Help
-----------
@@ -124,13 +176,6 @@ Library is under development and contributions are welcome!
4. Send a pull request. Make sure to add yourself to AUTHORS.
-Background
-----------
-
-API structure inspired by python-requests and werkzeug libraries.
-Some code is from my mailcube.ru experience.
-
-
See also
--------
diff --git a/emails/__init__.py b/emails/__init__.py
index 1a49942..518c474 100644
--- a/emails/__init__.py
+++ b/emails/__init__.py
@@ -23,10 +23,12 @@ More examples is at .
"""
__title__ = 'emails'
-__version__ = '0.1.13'
+__version__ = '0.2'
__author__ = 'Sergey Lavrinenko'
__license__ = 'Apache 2.0'
-__copyright__ = 'Copyright 2013 Sergey Lavrinenko'
+__copyright__ = 'Copyright 2013-2015 Sergey Lavrinenko'
+
+USER_AGENT = 'python-emails/%s' % __version__
from .message import Message, html
from .utils import MessageID
diff --git a/emails/exc.py b/emails/exc.py
new file mode 100644
index 0000000..377b4d4
--- /dev/null
+++ b/emails/exc.py
@@ -0,0 +1,4 @@
+# encoding: utf-8
+
+class HTTPLoaderError(Exception):
+ pass
\ No newline at end of file
diff --git a/emails/loader/__init__.py b/emails/loader/__init__.py
index bbdfe91..4b7bf91 100644
--- a/emails/loader/__init__.py
+++ b/emails/loader/__init__.py
@@ -1,44 +1,99 @@
# encoding: utf-8
-import os, os.path
-import logging
-from .htmlloader import HTTPLoader
-from .fileloader import FileSystemLoader, ZipLoader
-from .stylesheets import PageStylesheets
+import os
+import os.path
+from emails.loader.helpers import guess_charset
+from emails.compat import to_unicode
+from emails.compat import urlparse
+from emails import Message
+from emails.utils import fetch_url
+from emails.loader import local_store
-def from_url(url, **kwargs):
- loader = HTTPLoader()
- loader.load_url(url=url, **kwargs)
- return loader
+def from_url(url, message_params=None, requests_params=None, **kwargs):
+
+ def _make_base_url(url):
+ # /a/b.html -> /a
+ p = list(urlparse.urlparse(url))[:5]
+ p[2] = os.path.split(p[2])[0]
+ return urlparse.urlunsplit(p)
+
+ # Load html page
+ r = fetch_url(url, requests_args=requests_params)
+ html = r.content
+ html = to_unicode(html, charset=guess_charset(r.headers, html))
+ html = html.replace('\r\n', '\n') # Remove \r
+
+ message_params = message_params or {}
+ message = Message(html=html, **message_params)
+ message.create_transformer(requests_params=requests_params,
+ base_url=_make_base_url(url))
+ message.transformer.load_and_transform(**kwargs)
+ message.transformer.save()
+ return message
load_url = from_url
+
+def from_directory(directory, index_file=None, message_params=None, **kwargs):
+
+ store = local_store.FileSystemLoader(searchpath=directory)
+ index_file_name = store.find_index_file(index_file)
+ dirname, _ = os.path.split(index_file_name)
+ if dirname:
+ store.base_path = dirname
+
+ message_params = message_params or {}
+ message = Message(html=store[index_file_name], **message_params)
+ message.create_transformer(local_loader=store, requests_params=kwargs.get('requests_params'))
+ message.transformer.load_and_transform(**kwargs)
+ message.transformer.save()
+ return message
+
+
def from_file(filename, **kwargs):
return from_directory(directory=os.path.dirname(filename), index_file=os.path.basename(filename), **kwargs)
-def from_directory(directory, index_file=None, **kwargs):
- loader = HTTPLoader()
- local_loader = FileSystemLoader(searchpath=directory)
- index_file_name = local_loader.find_index_file(index_file)
- dirname, basename = os.path.split(index_file_name)
+
+def from_zip(zip_file, message_params=None, **kwargs):
+ store = local_store.ZipLoader(file=zip_file)
+ index_file_name = store.find_index_file()
+ dirname, index_file_name = os.path.split(index_file_name)
if dirname:
- local_loader.base_path = dirname
- loader.load_file(local_loader[basename], local_loader=local_loader, **kwargs)
- return loader
+ store.base_path = dirname
-def from_zip(zip_file, **kwargs):
- loader = HTTPLoader()
- local_store = ZipLoader(file=zip_file)
- index_file_name = local_store.find_index_file()
- dirname, basename = os.path.split(index_file_name)
- if dirname:
- local_store.base_path = dirname
- logging.debug('from_zip: found index file: %s', index_file_name)
- loader.load_file(local_store[basename], local_loader=local_store, **kwargs)
- return loader
+ message_params = message_params or {}
+ message = Message(html=store[index_file_name], **message_params)
+ message.create_transformer(local_loader=store, requests_params=kwargs.get('requests_params'))
+ message.transformer.load_and_transform(**kwargs)
+ message.transformer.save()
+ return message
-def from_string(html, css=None, **kwargs):
- loader = HTTPLoader()
- loader.load_string(html=html, css=css, **kwargs)
- return loader
+def from_html(html, base_url=None, message_params=None, **kwargs):
+ message_params = message_params or {}
+ message = Message(html=html, **message_params)
+ message.create_transformer(requests_params=kwargs.get('requests_params'), base_url=base_url)
+ message.transformer.load_and_transform(**kwargs)
+ message.transformer.save()
+ return message
+
+from_string = from_html
+
+
+def from_rfc822(msg, message_params=None, **kw):
+
+ store = local_store.MsgLoader(msg=msg)
+ text = store.get_source('__index.txt')
+ html = store.get_source('__index.html')
+
+ message_params = message_params or {}
+ message = Message(html=html, text=text, **message_params)
+ if html:
+ message.create_transformer(local_loader=store, **kw)
+ message.transformer.load_and_transform()
+ message.transformer.save()
+ else:
+ # TODO: add attachments for text-only message
+ pass
+
+ return message
\ No newline at end of file
diff --git a/emails/loader/cssinliner.py b/emails/loader/cssinliner.py
deleted file mode 100644
index aa58b8e..0000000
--- a/emails/loader/cssinliner.py
+++ /dev/null
@@ -1,170 +0,0 @@
-# -*- coding: utf-8 -*-
-# adapted from https://github.com/kgn/cssutils/blob/master/examples/style.py
-from __future__ import unicode_literals, print_function
-import logging
-
-from cssutils.css import CSSStyleSheet, CSSStyleDeclaration, CSSStyleRule
-from cssutils import CSSParser
-from lxml import etree
-
-from emails.compat import to_unicode, string_types
-import emails
-
-
-# Workaround the missing python3-cssselect package
-# If no system-installed cssselect library found, use one from our distribution
-try:
- import cssselect
-except ImportError:
- import sys, os.path
- my_packages = os.path.dirname(emails.packages.__file__)
- sys.path.insert(0, my_packages)
- import cssselect
-
-from lxml.cssselect import CSSSelector, ExpressionError
-
-class CSSInliner:
-
- NONVISUAL_TAGS = ['html', 'head', 'title', 'meta', 'link', 'script']
-
- DEBUG = False
-
- def __init__(self, base_url=None, css=None):
-
- self.stylesheet = CSSStyleSheet(href=base_url)
- self.base_url = base_url
- if css:
- self.add_css(css)
-
- def add_css(self, css, href=None):
-
- if isinstance(css, string_types):
- css = CSSParser().parseString(css, href=href) # Распарсим файл
-
- for rule in css:
- self.stylesheet.add(rule)
-
- def log(self, level, *msg):
- if self.DEBUG:
- print(('%s- %s' % (level * '\t ', ' '.join((to_unicode(m or '') for m in msg)))))
-
- def styleattribute(self, element):
- """returns css.CSSStyleDeclaration of inline styles, for html: @style"""
- cssText = element.get('style')
- if cssText:
- try:
- return CSSStyleDeclaration(cssText=cssText)
- except Exception as e:
- # Sometimes here's error like "COLOR: ;"
- logging.exception('Exception in styleattribute %s', cssText)
- return None
- else:
- return None
-
- def getView(self, document, sheet, media='all', name=None, styleCallback=None):
- """
- document
- a DOM document, currently an lxml HTML document
- sheet
- a CSS StyleSheet object, currently cssutils sheet
- media: optional
- TODO: view for which media it should be
- name: optional
- TODO: names of sheets only
- styleCallback: optional
- should return css.CSSStyleDeclaration of inline styles, for html
- a style declaration for ``element@style``. Gets one parameter
- ``element`` which is the relevant DOMElement
-
- returns style view
- a dict of {DOMElement: css.CSSStyleDeclaration} for html
- """
-
- styleCallback = styleCallback or self.styleattribute
-
- _unmergable_rules = CSSStyleSheet()
-
- view = {}
- specificities = {} # needed temporarily
-
- # TODO: filter rules simpler?, add @media
- rules = (rule for rule in sheet if rule.type == rule.STYLE_RULE)
- for rule in rules:
- for selector in rule.selectorList:
- self.log(0, 'SELECTOR', selector.selectorText)
- # TODO: make this a callback to be able to use other stuff than lxml
- try:
- cssselector = CSSSelector(selector.selectorText)
- except (ExpressionError, NotImplementedError) as e:
- _unmergable_rules.add(CSSStyleRule(selectorText=selector.selectorText,
- style=rule.style))
- continue
-
- matching = cssselector.evaluate(document)
-
- for element in matching:
-
- if element.tag in self.NONVISUAL_TAGS:
- continue
-
- # add styles for all matching DOM elements
- self.log(1, 'ELEMENT', id(element), element.text)
-
- if element not in view:
- # add initial empty style declatation
- view[element] = CSSStyleDeclaration()
- specificities[element] = {}
-
- # and add inline @style if present
- inlinestyle = styleCallback(element)
- if inlinestyle:
- for p in inlinestyle:
- # set inline style specificity
- view[element].setProperty(p)
- specificities[element][p.name] = (1, 0, 0, 0)
-
- for p in rule.style:
- # update style declaration
- if p not in view[element]:
- # setProperty needs a new Property object and
- # MUST NOT reuse the existing Property
- # which would be the same for all elements!
- # see Issue #23
- view[element].setProperty(p.name, p.value, p.priority)
- specificities[element][p.name] = selector.specificity
- self.log(2, view[element].getProperty('color'))
-
- else:
- self.log(2, view[element].getProperty('color'))
- sameprio = (p.priority ==
- view[element].getPropertyPriority(p.name))
- if not sameprio and bool(p.priority) or (
- sameprio and selector.specificity >=
- specificities[element][p.name]):
- # later, more specific or higher prio
- view[element].setProperty(p.name, p.value, p.priority)
-
- _unmergable_css = _unmergable_rules.cssText
- if _unmergable_css:
- e = etree.Element('style')
- e.text = to_unicode(_unmergable_css, 'utf-8')
- body = document.find('body') or document
- body.insert(0, e) # add
- """
- if el.text:
- self.stylesheets.append(text=el.text, url=self.start_url)
-
- def iter_image_links(self):
- return (_ for _ in self._tags_with_images)
-
- def iter_a_links(self):
- return (_ for _ in self._a_links)
-
- def process_tag_with_link(self, el):
- """
- Process IMG SRC, TABLE BACKGROUND, ...
- """
- obj = self.tag_link_cls[el.tag](el, encoding=self.html_encoding)
- if obj.link is None:
- return
-
- self._tags_with_links.append(obj)
- if el.tag in self.TAGS_WITH_IMAGES:
- lnk = obj.link
- if lnk is not None:
- self._tags_with_images.append(obj)
- elif el.tag == 'a':
- self._a_links.append(obj)
-
- def attach_all_images(self):
- for obj in self.iter_image_links():
- lnk = obj.link
- if lnk:
- self.attach_image(uri=lnk, absolute_url=self.absolute_url(lnk))
-
- def attach_image(self, uri, absolute_url, subtype=None):
- if uri not in self.filestore:
- self.filestore.add(self.attached_image_cls(
- uri=uri,
- absolute_url=absolute_url,
- local_loader=self.local_loader,
- subtype=subtype,
- fetch_params=self.fetch_params))
-
- def process_tag_with_style(self, el):
- t = StyledTagWrapper(el)
- for p in t.uri_properties():
- obj = self.css_link_cls(p, updateme=t)
- self._tags_with_links.append(obj)
- self._tags_with_images.append(obj)
-
- def process_stylesheet_uri_property(self, prop):
- obj = self.css_link_cls(prop)
- self._tags_with_links.append(obj)
- self._tags_with_images.append(obj)
-
- def make_link_absolute(self, obj):
- link = obj.link
- if link:
- obj.link = self.absolute_url(link)
-
- def make_images_inline(self):
-
- found_links = set()
-
- for img in self.iter_image_links():
- link = img.link
- found_links.add(link)
- file = self.filestore.by_uri(link, img.link_history)
- img.link = "cid:%s" % file.filename
-
- for file in self.filestore:
- if file.uri in found_links:
- file.content_disposition = 'inline'
- else:
- logging.debug('make_images_inline %s=none', file.uri)
-
- def set_content_type_meta(self):
- _tree = self.html_tree
- new_document = helpers.set_content_type_meta(_tree, element_cls=etree.Element)
- if _tree != new_document:
- # document may be updated here (i.e. html tag added)
- self.html_tree = new_document
-
- def insert_big_stylesheet(self):
- return helpers.add_body_stylesheet(self.html_tree, element_cls=etree.Element,
- tag="body", cssText="")
-
- def absolute_url(self, url, base_url=None):
-
- # In: some url
- # Out: (absolute_url, relative_url) based on self._base_url
-
- if base_url is None:
- base_url = self.base_url
-
- if base_url is None:
- return url
-
- parsed_url = urlparse.urlsplit(url)
- if parsed_url.scheme:
- # is absolute_url
- return url
- else:
- # http://xxx.com/../../style.css -> http://xxx.com/style.css
- # см. http://teethgrinder.co.uk/perm.php?a=Normalize-URL-path-python
- joined = urlparse.urljoin(self.base_url, url)
- url = urlparse.urlparse(joined)
- path = posixpath.normpath(url[2])
- return urlparse.urlunparse((url.scheme, url.netloc, path, url.params, url.query, url.fragment))
-
- def doinlinecss(self):
- self.html_tree = CSSInliner(css=self.stylesheets.stylesheet).transform(html=self.html_tree)
-
- @property
- def html(self):
- self.stylesheets.update_tag()
- self._html = etree.tostring(self.html_tree, encoding=self.encoding, method='xml')
- return to_unicode(self._html, self.encoding)
-
- @property
- def attachments_dict(self):
- return list(self.filestore.as_dict())
-
- def save_to_file(self, filename):
- #
- # Not very good example of link walking and file rename
- #
-
- path = os.path.abspath(filename)
- # Save images locally and replace all links to images in html
- files_dir = '_files'
- _rename_map = {}
-
- for obj in self.iter_image_links():
- uri = obj.link
- if uri is None:
- continue
- _new_uri = _rename_map.get(uri, None)
- if _new_uri is None:
- file = self.filestore.by_uri(uri, synonims=obj.link_history)
- if file is None:
- logging.warning(
- 'file "%s" not found in attachments, this should not happen. skipping', uri)
- continue
- _new_uri = _rename_map[uri] = os.path.join(files_dir, file.filename)
- obj.link = _new_uri
-
- try:
- os.makedirs(files_dir)
- except OSError:
- pass
- for attach in self.filestore:
- attach.fetch()
- new_uri = _rename_map.get(attach.uri)
- if new_uri:
- attach.uri = new_uri
- open(new_uri, 'wb').write(attach.data)
-
- f = open(filename, 'wb')
- f.write(to_bytes(self.html, 'utf-8'))
- f.close()
-
diff --git a/emails/loader/fileloader.py b/emails/loader/local_store.py
similarity index 61%
rename from emails/loader/fileloader.py
rename to emails/loader/local_store.py
index d244c4e..bb97e42 100644
--- a/emails/loader/fileloader.py
+++ b/emails/loader/local_store.py
@@ -1,15 +1,15 @@
# encoding: utf-8
from __future__ import unicode_literals
import logging
+import mimetypes
import os
from os import path
import errno
from zipfile import ZipFile
+import email
from emails.compat import to_unicode, string_types
-# FileSystemLoader adapted from jinja2.loaders
-
class FileNotFound(Exception):
pass
@@ -74,6 +74,8 @@ class BaseLoader(object):
raise FileNotFound('index html')
+# FileSystemLoader from jinja2.loaders
+
class FileSystemLoader(BaseLoader):
"""Loads templates from the file system. This loader can find templates
in folders on the file system and is the preferred way to load them.
@@ -155,11 +157,8 @@ class ZipLoader(BaseLoader):
def get_source(self, name):
- logging.debug('ZipLoader.get_source %s', name)
-
if self.base_path:
name = path.join(self.base_path, name)
- logging.debug('ZipLoader.get_source has base_path, result name is %s', name)
self._unpack_zip()
@@ -173,16 +172,117 @@ class ZipLoader(BaseLoader):
original_name = self._filenames.get(name)
- logging.debug('ZipLoader.get_source original_name=%s', original_name)
-
if original_name is None:
raise FileNotFound(name)
data = self.zipfile.read(original_name)
- logging.debug('ZipLoader.get_source returns %s bytes', len(data))
return data, name
def list_files(self):
self._unpack_zip()
return sorted(self._filenames)
+
+
+class MsgLoader(BaseLoader):
+ """
+ Load files from email.Message
+ Thanks to
+ http://blog.magiksys.net/parsing-email-using-python-content
+ """
+
+ common_charsets = ['ascii', 'utf-8', 'utf-16', 'windows-1252', 'cp850', 'windows-1251']
+
+ def __init__(self, msg, base_path=None):
+ if isinstance(msg, string_types):
+ self.msg = email.message_from_string(msg)
+ else:
+ self.msg = msg
+ self.base_path = base_path
+ self._html_files = []
+ self._text_files = []
+ self._files = {}
+
+
+ def decode_text(self, text, charset=None):
+ if charset:
+ try:
+ return text.decode(charset), charset
+ except UnicodeError:
+ pass
+ for charset in self.common_charsets:
+ try:
+ return text.decode(charset), charset
+ except UnicodeError:
+ pass
+ return text, None
+
+ def clean_content_id(self, content_id):
+ if content_id.startswith('<'):
+ content_id = content_id[1:]
+ if content_id.endswith('>'):
+ content_id = content_id[:-1]
+ return content_id
+
+ def extract_part_text(self, part):
+ return self.decode_text(part.get_payload(decode=True), charset=part.get_param('charset'))[0]
+
+ def add_html_part(self, part):
+ name = '__index.html'
+ self._files[name] = {'data': self.extract_part_text(part),
+ 'filename': name,
+ 'content_type': part.get_content_type()}
+
+ def add_text_part(self, part):
+ name = '__index.txt'
+ self._files[name] = {'data': self.extract_part_text(part),
+ 'filename': name,
+ 'content_type': part.get_content_type()}
+
+ def add_another_part(self, part):
+ counter = 1
+ f = {}
+ content_id = part['Content-ID']
+ if content_id:
+ f['filename'] = self.clean_content_id(content_id)
+ f['inline'] = True
+ else:
+ filename = part.get_filename()
+ if not filename:
+ ext = mimetypes.guess_extension(part.get_content_type())
+ if not ext:
+ # Use a generic bag-of-bits extension
+ ext = '.bin'
+ filename = 'part-%03d%s' % (counter, ext)
+ counter += 1
+ f['filename'] = filename
+ f['content_type'] = part.get_content_type()
+ f['data'] = part.get_payload(decode=True)
+ self._files[f['filename']] = f
+
+ def _parse_msg(self):
+ for part in self.msg.walk():
+ content_type = part.get_content_type()
+
+ if content_type.startswith('multipart/'):
+ continue
+
+ if content_type == 'text/html':
+ self.add_html_part(part)
+ continue
+
+ if content_type == 'text/plain':
+ self.add_text_part(part)
+ continue
+
+ self.add_another_part(part)
+
+ def get_source(self, name):
+ self._parse_msg()
+ f = self._files.get(name)
+ if f:
+ return f['data'], name
+ return None, name
+
+ def list_files(self):
+ return self._files
\ No newline at end of file
diff --git a/emails/loader/stylesheets.py b/emails/loader/stylesheets.py
deleted file mode 100644
index c5ea8fb..0000000
--- a/emails/loader/stylesheets.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# encoding: utf-8
-from __future__ import unicode_literals, print_function
-import logging
-
-from cssutils.css import CSSStyleSheet
-from cssutils import CSSParser
-import cssutils
-
-from emails.compat import to_unicode
-
-
-class PageStylesheets:
-
- """
- Store all html page styles and generates concatenated stylesheet
- """
-
- def __init__(self):
- self.urls = set()
- self._uri_properties = []
- self.sheets = []
- self.dirty = True
- self.element = None
-
- def update_tag(self):
- if self.element is not None:
- self._concatenate_sheets()
- cssText = self._cached_stylesheet.cssText
- cssText = cssText and to_unicode(cssText, 'utf-8') or ''
- self.element.text = cssText
-
- def attach_tag(self, element):
- self.element = element
-
- def append(self, url=None, text=None, absolute_url=None, local_loader=None):
- if (url is not None) and (url in self.urls):
- logging.debug('stylesheet url duplicate: %s', url)
- return
- self.sheets.append({'url': url, 'text': text, 'absolute_url': absolute_url or url,
- 'local_loader': local_loader})
- self.dirty = True
-
- def _concatenate_sheets(self):
- if self.dirty or (self._cached_stylesheet is None):
- r = CSSStyleSheet()
- uri_properties = []
-
- for d in self.sheets:
- local_loader = d.get('local_loader', None)
- text = d.get('text', None)
- uri = d.get('uri', None)
- absolute_url = d.get('absolute_url', None)
-
- if (text is None) and local_loader and uri:
- text = local_loader[uri]
-
- if text:
- sheet = CSSParser().parseString(text, href=absolute_url)
- else:
- sheet = cssutils.parseUrl(href=absolute_url)
-
- for rule in sheet:
- r.add(rule)
- for p in _get_rule_uri_properties(rule):
- uri_properties.append(p)
-
- self._uri_properties = uri_properties
- self._cached_stylesheet = r
- self.dirty = False
-
- @property
- def stylesheet(self):
- self._concatenate_sheets()
- return self._cached_stylesheet
-
- @property
- def uri_properties(self):
- self._concatenate_sheets()
- return self._uri_properties
-
-
-class StyledTagWrapper:
-
- def __init__(self, el):
- self.el = el
- self.style = CSSParser().parseStyle(el.get('style'))
-
- def update(self):
- cssText = self.style.cssText
- if isinstance(cssText, str):
- cssText = to_unicode(cssText, 'utf-8')
- self.el.set('style', cssText)
-
- def uri_properties(self):
- for p in self.style.getProperties(all=True):
- for v in p.propertyValue:
- if v.type == 'URI':
- yield v
-
-
-# Stuff for extracting 'uri-properties' from CSS
-# Inspired by cssutils examples
-
-def _style_declarations(base):
- """recursive generator to find all CSSStyleDeclarations"""
- if hasattr(base, 'cssRules'):
- for rule in base.cssRules:
- for s in _style_declarations(rule):
- yield s
- elif hasattr(base, 'style'):
- yield base.style
-
-
-def _get_rule_uri_properties(rule):
- for style in _style_declarations(rule):
- for p in style.getProperties(all=True):
- for v in p.propertyValue:
- if v.type == 'URI':
- yield v
-
-
-def get_stylesheets_uri_properties(sheet):
- for rule in sheet:
- for p in _get_rule_uri_properties(rule):
- yield p
diff --git a/emails/loader/wrappers.py b/emails/loader/wrappers.py
deleted file mode 100644
index 15aca47..0000000
--- a/emails/loader/wrappers.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# encoding: utf-8
-
-# tag-with-link wrapper
-from __future__ import unicode_literals
-import logging
-from emails.compat import OrderedSet, to_unicode
-
-
-class ElementWithLink(object):
-
- LINK_ATTR_NAME = None
-
- def __init__(self, el, encoding=None):
- self.el = el
- self._link_history = OrderedSet()
- self.encoding = encoding
-
- def get_link(self):
- r = self.el.get(self.LINK_ATTR_NAME)
- if self.encoding:
- r = to_unicode(r, self.encoding)
- return r
-
- def set_link(self, new):
- _old = self.get_link()
- if _old != new:
- logging.debug('Update link %s => %s ', _old, new)
- self.el.set(self.LINK_ATTR_NAME, new)
- self._link_history.add(_old)
-
- link = property(get_link, set_link)
-
- @classmethod
- def make(cls, attr):
- def wrapper(el, encoding):
- r = cls(el, encoding=encoding)
- r.LINK_ATTR_NAME = attr
- return r
- return wrapper
-
- @property
- def link_history(self):
- return self._link_history
-
-
-class A_link(ElementWithLink):
- # el is lxml.Element
- LINK_ATTR_NAME = 'href'
-
-
-class Link_link(ElementWithLink):
- # el is lxml.Element
- LINK_ATTR_NAME = 'href'
-
-
-class IMG_link(ElementWithLink):
- # el is lxml.Element
- LINK_ATTR_NAME = 'src'
-
-
-class Background_link(ElementWithLink):
- LINK_ATTR_NAME = 'background'
-
-
-class CSS_link(ElementWithLink):
-
- # el is cssutils style property
-
- def __init__(self, el, updateme=None, encoding=None):
- ElementWithLink.__init__(self, el)
- self.updateme = updateme
- self.encoding = encoding
-
- def get_link(self):
- r = self.el.uri
- if self.encoding:
- r = to_unicode(self.el.uri, self.encoding)
- return r
-
- def set_link(self, new):
- _old = self.el.uri
- if _old != new:
- logging.debug('Update link %s => %s ', _old, new)
- self.el.uri = new
- self._link_history.add(_old)
- if self.updateme:
- self.updateme.update()
-
- link = property(get_link, set_link)
-
-
-def TAG_WRAPPER(attr):
- return ElementWithLink.make(attr)
-
-
-CSS_WRAPPER = CSS_link
diff --git a/emails/message.py b/emails/message.py
index 215aa9a..6f27a4d 100644
--- a/emails/message.py
+++ b/emails/message.py
@@ -7,28 +7,19 @@ from functools import wraps
from dateutil.parser import parse as dateutil_parse
from email.header import Header
from email.utils import formatdate, getaddresses
-
from emails.compat import string_types, to_unicode, is_callable, to_bytes
-
-from .utils import SafeMIMEText, SafeMIMEMultipart, sanitize_address, parse_name_and_email
+from .utils import (SafeMIMEText, SafeMIMEMultipart, sanitize_address,
+ parse_name_and_email, load_email_charsets,
+ encode_header as encode_header_)
from .smtp import ObjectFactory, SMTPBackend
from .store import MemoryFileStore, BaseFile
from .signers import DKIMSigner
-from .utils import load_email_charsets
-
load_email_charsets() # sic!
-ROOT_PREAMBLE = 'This is a multi-part message in MIME format.\n'
-
-
class BadHeaderError(ValueError):
pass
-# Header names that contain structured address data (RFC #5322)
-ADDRESS_HEADERS = set(['from', 'sender', 'reply-to', 'to', 'cc', 'bcc', 'resent-from', 'resent-sender', 'resent-to',
- 'resent-cc', 'resent-bcc'])
-
def renderable(f):
@wraps(f)
@@ -48,23 +39,22 @@ class IncompleteMessage(Exception):
pass
-class Message(object):
- """
- Email class
- message = HtmlEmail()
- Message parts:
- * html
- * text
- * attachments
+class BaseMessage(object):
"""
+ Base email message with html part, text part and attachments.
+ """
+
+ ROOT_PREAMBLE = 'This is a multi-part message in MIME format.\n'
+
+ # Header names that contain structured address data (RFC #5322)
+ ADDRESS_HEADERS = set(['from', 'sender', 'reply-to', 'to', 'cc', 'bcc',
+ 'resent-from', 'resent-sender', 'resent-to',
+ 'resent-cc', 'resent-bcc'])
attachment_cls = BaseFile
- dkim_cls = DKIMSigner
- smtp_pool_factory = ObjectFactory
- smtp_cls = SMTPBackend
filestore_cls = MemoryFileStore
def __init__(self,
@@ -87,20 +77,25 @@ class Message(object):
self.set_mail_from(mail_from)
self.set_mail_to(mail_to)
self.set_headers(headers)
- self.set_html(html=html) # , url=self.html_from_url)
- self.set_text(text=text) # , url=self.text_from_url)
+ self.set_html(html=html)
+ self.set_text(text=text)
self.render_data = {}
- self._dkim_signer = None
+ self.after_build = None
+
if attachments:
for a in attachments:
self.attachments.add(a)
- self.after_build = None
-
def set_mail_from(self, mail_from):
# In: ('Alice', '' )
self._mail_from = mail_from and parse_name_and_email(mail_from) or None
+ def get_mail_from(self):
+ # Out: ('Alice', '') or None
+ return self._mail_from
+
+ mail_from = property(get_mail_from, set_mail_from)
+
def set_mail_to(self, mail_to):
# Now we parse only one to-addr
# TODO: parse list of to-addrs
@@ -121,25 +116,40 @@ class Message(object):
self._html = html
self._html_url = url
+ def get_html(self):
+ return self._html
+
+ html = property(get_html, set_html)
+
def set_text(self, text, url=None):
if hasattr(text, 'read'):
text = text.read()
self._text = text
self._text_url = url
- def attach(self, **kwargs):
- if 'content_disposition' not in kwargs:
- kwargs['content_disposition'] = 'attachment'
- self.attachments.add(kwargs)
+ def get_text(self):
+ return self._text
+
+ text = property(get_text, set_text)
@classmethod
def from_loader(cls, loader, template_cls=None, **kwargs):
"""
- Get html and attachments from HTTPLoader
+ Get html and attachments from Loader
"""
- message = cls(html=template_cls and template_cls(loader.html) or loader.html, **kwargs)
- for att in loader.filestore:
- message.attach(**att.as_dict())
+
+ html = loader.html
+ if html and template_cls:
+ html = template_cls(html)
+
+ text = loader.text
+ if text and template_cls:
+ text = template_cls(text)
+
+ message = cls(html=html, text=text, **kwargs)
+
+ for attachment in loader.attachments:
+ message.attach(**attachment.as_dict())
return message
@property
@@ -164,12 +174,6 @@ class Message(object):
def render(self, **kwargs):
self.render_data = kwargs
- @property
- def attachments(self):
- if self._attachments is None:
- self._attachments = self.filestore_cls(self.attachment_cls)
- return self._attachments
-
def set_date(self, value):
if isinstance(value, string_types):
_d = dateutil_parse(value)
@@ -197,13 +201,7 @@ class Message(object):
return is_callable(mid) and mid() or mid
def encode_header(self, value):
- value = to_unicode(value, charset=self.charset)
- if isinstance(value, string_types):
- value = value.rstrip()
- _r = Header(value, self.charset)
- return str(_r)
- else:
- return value
+ return encode_header_(value, self.charset)
def encode_name_header(self, realname, email):
if realname:
@@ -222,18 +220,29 @@ class Message(object):
if '\n' in value or '\r' in value:
raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (value, key))
- if key.lower() in ADDRESS_HEADERS:
+ if key.lower() in self.ADDRESS_HEADERS:
value = ', '.join(sanitize_address(addr, self.charset)
for addr in getaddresses((value,)))
msg[key] = encode and self.encode_header(value) or value
+ @property
+ def attachments(self):
+ if self._attachments is None:
+ self._attachments = self.filestore_cls(self.attachment_cls)
+ return self._attachments
+
+ def attach(self, **kwargs):
+ if 'content_disposition' not in kwargs:
+ kwargs['content_disposition'] = 'attachment'
+ self.attachments.add(kwargs)
+
def _build_message(self, message_cls=None):
message_cls = message_cls or SafeMIMEMultipart
msg = message_cls()
- msg.preamble = ROOT_PREAMBLE
+ msg.preamble = self.ROOT_PREAMBLE
self.set_header(msg, 'Date', self.message_date, encode=False)
self.set_header(msg, 'Message-ID', self.message_id(), encode=False)
@@ -255,8 +264,11 @@ class Message(object):
mail_to = self._mail_to and self.encode_name_header(*self._mail_to[0]) or None
self.set_header(msg, 'To', mail_to, encode=False)
+ msgrel = SafeMIMEMultipart('related')
+ msg.attach(msgrel)
+
msgalt = SafeMIMEMultipart('alternative')
- msg.attach(msgalt)
+ msgrel.attach(msgalt)
_text = self.text_body
_html = self.html_body
@@ -275,34 +287,23 @@ class Message(object):
msgalt.attach(msghtml)
for f in self.attachments:
- msgfile = f.mime
- if msgfile:
- msg.attach(msgfile)
+ part = f.mime
+ if part:
+ if f.is_inline:
+ msgrel.attach(part)
+ else:
+ msg.attach(part)
if self.after_build:
self.after_build(self, msg)
return msg
- def message(self, message_cls=None):
- msg = self._build_message(message_cls=message_cls)
- if self._dkim_signer:
- msg_str = msg.as_string()
- dkim_header = self._dkim_signer.get_sign_header(to_bytes(msg_str))
- if dkim_header:
- msg._headers.insert(0, dkim_header)
- return msg
- def as_string(self):
- # self.as_string() is not equialent self.message().as_string()
- # self.as_string() gets one less message-to-string conversions for dkim
- msg = self._build_message()
- r = msg.as_string()
- if self._dkim_signer:
- dkim_header = self._dkim_signer.get_sign(to_bytes(r))
- if dkim_header:
- r = dkim_header + r
- return r
+class MessageSendMixin(object):
+
+ smtp_pool_factory = ObjectFactory
+ smtp_cls = SMTPBackend
@property
def smtp_pool(self):
@@ -311,9 +312,6 @@ class Message(object):
pool = self._smtp_pool = self.smtp_pool_factory(cls=self.smtp_cls)
return pool
- def dkim(self, **kwargs):
- self._dkim_signer = self.dkim_cls(**kwargs)
-
def send(self,
to=None,
set_mail_to=True,
@@ -361,7 +359,7 @@ class Message(object):
from_addr = self._mail_from[1]
if not from_addr:
- raise ValueError('No from-addr')
+ raise ValueError('No "from" addr')
params = dict(from_addr=from_addr,
to_addrs=[to_addr, ],
@@ -376,6 +374,105 @@ class Message(object):
return response[0]
+class MessageTransformerMixin(object):
+
+ transformer_cls = None
+
+ def create_transformer(self, **kw):
+ cls = self.transformer_cls
+ if cls is None:
+ from emails.transformer import MessageTransformer
+ cls = MessageTransformer
+
+ self._transformer = cls(message=self, **kw)
+ return self._transformer
+
+ def destroy_transformer(self):
+ self._transformer = None
+
+ @property
+ def transformer(self):
+ t = getattr(self, '_transformer', None)
+ if t is None:
+ t = self.create_transformer()
+ return t
+
+
+class Message(BaseMessage, MessageSendMixin, MessageTransformerMixin):
+ """
+ Email message with:
+ - DKIM signer
+ - smtp send
+ - Message.transformer object
+ """
+
+ dkim_cls = DKIMSigner
+
+ def __init__(self, **kwargs):
+ BaseMessage.__init__(self, **kwargs)
+ self._dkim_signer = None
+ self.after_build = None
+
+ def dkim(self, **kwargs):
+ self._dkim_signer = self.dkim_cls(**kwargs)
+
+ def set_html(self, **kw):
+ # When html set, remove old transformer
+ self.destroy_transformer()
+ super(Message, self).set_html(**kw)
+
+ def as_message(self, message_cls=None):
+ msg = self._build_message(message_cls=message_cls)
+ if self._dkim_signer:
+ msg_str = msg.as_string()
+ dkim_header = self._dkim_signer.get_sign_header(to_bytes(msg_str))
+ if dkim_header:
+ msg._headers.insert(0, dkim_header)
+ return msg
+
+ message = as_message
+
+ def as_string(self):
+ # self.as_string() is not equialent self.message().as_string()
+ # self.as_string() gets one less message-to-string conversions for dkim
+ msg = self._build_message()
+ r = msg.as_string()
+ if self._dkim_signer:
+ dkim_header = self._dkim_signer.get_sign(to_bytes(r))
+ if dkim_header:
+ r = dkim_header + r
+ return r
+
+
def html(**kwargs):
return Message(**kwargs)
+
+class DjangoMessageProxy(object):
+
+ """
+ Class looks like django.core.mail.EmailMessage for standard django email backend.
+
+ Example usage:
+
+ message = emails.Message(html='...', subject='...', mail_from='robot@company.ltd')
+ connection = django.core.mail.get_connection()
+
+ message.set_mail_to('somebody@somewhere.net')
+ connection.send_messages([DjangoMessageProxy(message), ])
+ """
+
+ def __init__(self, message, recipients=None, context=None):
+ self._message = message
+ self._recipients = recipients
+ self._context = context and context.copy() or {}
+
+ self.from_email = message.mail_from[1]
+ self.encoding = message.charset
+
+ def recipients(self):
+ return self._recipients or [r[1] for r in self._message.mail_to]
+
+ def message(self):
+ self._message.render(**self._context)
+ return self._message.message()
diff --git a/emails/smtp/backend.py b/emails/smtp/backend.py
index 31098ab..c8581f3 100644
--- a/emails/smtp/backend.py
+++ b/emails/smtp/backend.py
@@ -1,11 +1,10 @@
# encoding: utf-8
from __future__ import unicode_literals
-__all__ = [ 'SMTPSender' ]
+__all__ = ['SMTPBackend']
import smtplib
import logging
-import threading
from functools import wraps
from .client import SMTPResponse, SMTPClientWithResponse, SMTPClientWithResponse_SSL
@@ -31,25 +30,19 @@ class SMTPBackend:
def __init__(self,
- user=None,
- password=None,
ssl=False,
- tls=False,
- debug=False,
fail_silently=True,
**kwargs):
self.smtp_cls = ssl and self.connection_ssl_cls or self.connection_cls
- self.debug = debug
+
self.ssl = ssl
- self.tls = tls
+ self.tls = kwargs.get('tls')
if self.ssl and self.tls:
raise ValueError(
"ssl/tls are mutually exclusive, so only set "
"one of those settings to True.")
- self.user = user
- self.password = password
if 'timeout' not in kwargs:
kwargs['timeout'] = self.DEFAULT_SOCKET_TIMEOUT
self.smtp_cls_kwargs = kwargs
@@ -59,10 +52,8 @@ class SMTPBackend:
self.fail_silently = fail_silently
self.connection = None
#self.local_hostname=DNS_NAME.get_fqdn()
- self._lock = threading.RLock()
def open(self):
- #logger.debug('SMTPSender _connect')
if self.connection is None:
self.connection = self.smtp_cls(parent=self, **self.smtp_cls_kwargs)
self.connection.initialize()
@@ -83,7 +74,6 @@ class SMTPBackend:
finally:
self.connection = None
-
def make_response(self, exception=None):
return self.response_cls(host=self.host, port=self.port, exception=exception)
@@ -105,21 +95,17 @@ class SMTPBackend:
def sendmail(self, from_addr, to_addrs, msg, mail_options=[], rcpt_options=[]):
- if not to_addrs: return False
+ if not to_addrs:
+ return False
if not isinstance(to_addrs, (list, tuple)):
to_addrs = [to_addrs, ]
- #from_addr = sanitize_address(from_addr, email_message.encoding)
- #to_addrs = [sanitize_address(addr, email_message.encoding) for addr in to_addrs]
- #message = email_message.message()
- #charset = message.get_charset().get_output_charset() if message.get_charset() else 'utf-8'
-
try:
self.open()
except (IOError, smtplib.SMTPException) as e:
logger.exception("Error connecting smtp server")
- response = self.make_response(exception = e)
+ response = self.make_response(exception=e)
if not self.fail_silently:
response.raise_if_needed()
return [response, ]
@@ -133,7 +119,6 @@ class SMTPBackend:
rcpt_options=rcpt_options)
if not self.fail_silently:
- [ r.raise_if_needed() for r in response ]
+ [r.raise_if_needed() for r in response]
return response
-
diff --git a/emails/smtp/client.py b/emails/smtp/client.py
index ab492a4..47fde2e 100644
--- a/emails/smtp/client.py
+++ b/emails/smtp/client.py
@@ -1,10 +1,8 @@
# encoding: utf-8
-
-__all__ = [ 'SMTPResponse', 'SMTPClientWithResponse', 'SMTPClientWithResponse_SSL' ]
+__all__ = ['SMTPResponse', 'SMTPClientWithResponse', 'SMTPClientWithResponse_SSL']
from smtplib import _have_ssl, SMTP
import smtplib
-
import logging
logger = logging.getLogger(__name__)
@@ -17,7 +15,6 @@ class SMTPResponse(object):
self.ssl = ssl
self.responses = []
self.exception = exception
- #self.complete = False
self.success = None
self.from_addr = None
self.esmtp_opts = None
@@ -28,7 +25,7 @@ class SMTPResponse(object):
self.last_command = None
def set_status(self, command, code, text):
- self.responses.append( [command, code, text] )
+ self.responses.append([command, code, text])
self.status_code = code
self.status_text = text
self.last_command = command
@@ -36,7 +33,7 @@ class SMTPResponse(object):
def set_exception(self, exc):
self.exception = exc
- def raise_if_needed():
+ def raise_if_needed(self):
if self.exception:
raise self.exception
@@ -49,9 +46,6 @@ class SMTPResponse(object):
self.status_text.__repr__())
-#class SMTPCommandsLog:
-
-
class SMTPClientWithResponse(SMTP):
def __init__(self, parent, **kwargs):
@@ -59,19 +53,17 @@ class SMTPClientWithResponse(SMTP):
self.make_response = parent.make_response
self._last_smtp_response = (None, None)
self.tls = kwargs.pop('tls', False)
- self.debug = kwargs.pop('debug', 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.debug:
- self.set_debuglevel(1)
if self.tls:
- self.ehlo()
self.starttls()
- self.ehlo()
if self.user:
self.login(user=self.user, password=self.password)
self.ehlo_or_helo_if_needed()
@@ -92,7 +84,6 @@ class SMTPClientWithResponse(SMTP):
self._last_smtp_response = (code, msg)
return code, msg
-
def _send_one_mail(self, from_addr, to_addr, msg, mail_options=[], rcpt_options=[]):
esmtp_opts = []
@@ -140,18 +131,17 @@ class SMTPClientWithResponse(SMTP):
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:
- raise StopIteration
+ 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 ]
+ return [self._send_one_mail(from_addr, to_addr, msg, mail_options, rcpt_options) \
+ for to_addr in to_addrs]
@@ -160,18 +150,34 @@ if _have_ssl:
from smtplib import SMTP_SSL
import ssl
- class SMTPClientWithResponse_SSL(SMTPClientWithResponse, SMTP_SSL):
+ class SMTPClientWithResponse_SSL(SMTP_SSL, SMTPClientWithResponse):
+
+ def __init__(self, **kw):
+ args = {}
+ for k in ('host', 'port', 'local_hostname', 'keyfile', 'certfile', 'timeout'):
+ if k in kw:
+ args[k] = kw[k]
+ 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 quit(self):
"""Closes the connection to the email server."""
try:
- super(self, SMTPClientWithResponse_SSL).quit()
+ SMTPClientWithResponse.quit(self)
except (ssl.SSLError, 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 sendmail(self, *args, **kw):
+ return SMTPClientWithResponse.sendmail(self, *args, **kw)
+
else:
class SMTPClientWithResponse_SSL:
diff --git a/emails/smtp/factory.py b/emails/smtp/factory.py
index f01de0f..9e326f0 100644
--- a/emails/smtp/factory.py
+++ b/emails/smtp/factory.py
@@ -2,7 +2,7 @@
def simple_dict2str(d):
# Simple dict serializer
- return ";".join( [ "%s=%s" % (k, v) for (k, v) in d.items() ] )
+ return ";".join(["%s=%s" % (k, v) for (k, v) in d.items()])
_serializer = simple_dict2str
diff --git a/emails/store/file.py b/emails/store/file.py
index 852bd97..80a3985 100644
--- a/emails/store/file.py
+++ b/emails/store/file.py
@@ -10,8 +10,11 @@ import requests
from mimetypes import guess_type
from email.mime.base import MIMEBase
from email.encoders import encode_base64
+import emails
from emails.compat import urlparse
from emails.compat import string_types, to_bytes
+from emails.utils import fetch_url, encode_header
+
# class FileNotFound(Exception):
# pass
@@ -32,6 +35,8 @@ class BaseFile(object):
Store base "attachment-file" information.
"""
+ content_id_suffix = '@python.emails'
+
def __init__(self, **kwargs):
"""
uri and filename are connected properties.
@@ -42,12 +47,11 @@ class BaseFile(object):
self.absolute_url = kwargs.get('absolute_url', None) or self.uri
self.filename = kwargs.get('filename', None)
self.data = kwargs.get('data', None)
- self._mime_type = kwargs.get('mime_type', None)
- self._headers = kwargs.get('headers', None)
- self._content_disposition = kwargs.get('content_disposition', None)
- self.subtype = kwargs.get('subtype', None)
- self.local_loader = kwargs.get('local_loader', None)
- self.id = id
+ self._mime_type = kwargs.get('mime_type')
+ self._headers = kwargs.get('headers')
+ self._content_disposition = kwargs.get('content_disposition', 'attachment')
+ self.subtype = kwargs.get('subtype')
+ self.local_loader = kwargs.get('local_loader')
def as_dict(self, fields=None):
fields = fields or ('uri', 'absolute_url', 'filename', 'data',
@@ -119,21 +123,41 @@ class BaseFile(object):
content_disposition = property(get_content_disposition, set_content_disposition)
+ @property
+ def is_inline(self):
+ return self.content_disposition == 'inline'
+
+ @is_inline.setter
+ def is_inline(self, value):
+ if bool(value):
+ self.content_disposition = 'inline'
+ else:
+ self.content_disposition = 'attachment'
+
+ @property
+ def content_id(self):
+ return "{0}{1}".format(self.filename, self.content_id_suffix)
+
+ @staticmethod
+ def parse_content_id(cls, content_id):
+ if content_id.endswith(cls.content_id_suffix):
+ return {'filename': content_id[:-len(cls.content_id_suffix)]}
+ else:
+ return None
+
@property
def mime(self):
if self.content_disposition is None:
return None
_mime = getattr(self, '_cached_mime', None)
if _mime is None:
- filename = str(Header(self.filename, 'utf-8'))
- self._cached_mime = _mime = MIMEBase(*self.mime_type.split('/', 1))
+ filename_header = encode_header(self.filename)
+ self._cached_mime = _mime = MIMEBase(*self.mime_type.split('/', 1), name=filename_header)
_mime.set_payload(to_bytes(self.data))
encode_base64(_mime)
- _mime.add_header('Content-Disposition',
- self.content_disposition,
- filename=filename)
+ _mime.add_header('Content-Disposition', self.content_disposition, filename=filename_header)
if self.content_disposition == 'inline':
- _mime.add_header('Content-ID', '<{0}>'.format(filename))
+ _mime.add_header('Content-ID', '<%s>' % self.content_id)
return _mime
def reset_mime(self):
@@ -145,11 +169,9 @@ class BaseFile(object):
class LazyHTTPFile(BaseFile):
- def __init__(self, fetch_params=None, **kwargs):
+ def __init__(self, requests_args=None, **kwargs):
BaseFile.__init__(self, **kwargs)
- self.fetch_params = dict(allow_redirects=True, verify=False)
- if fetch_params:
- self.fetch_params.update(fetch_params)
+ self.requests_args = requests_args
self._fetched = False
def fetch(self):
@@ -162,7 +184,7 @@ class LazyHTTPFile(BaseFile):
self._data = data
return
- r = requests.get(self.absolute_url or self.uri, **self.fetch_params)
+ r = fetch_url(url=self.absolute_url or self.uri, requests_args=self.requests_args)
if r.status_code == 200:
self._data = r.content
self._headers = r.headers
diff --git a/emails/store/store.py b/emails/store/store.py
index e56ca39..1f563d8 100644
--- a/emails/store/store.py
+++ b/emails/store/store.py
@@ -18,7 +18,7 @@ class MemoryFileStore(FileStore):
if file_cls:
self.file_cls = file_cls
self._files = OrderedDict()
- self._filenames = set()
+ self._filenames = {}
def __contains__(self, k):
if isinstance(k, self.file_cls):
@@ -48,24 +48,26 @@ class MemoryFileStore(FileStore):
if v:
filename = v.filename
if filename and (filename in self._filenames):
- self._filenames.remove(filename)
+ del self._filenames[filename]
del self._files[uri]
- def unique_filename(self, filename):
+ def unique_filename(self, filename, uri=None):
- if filename not in self._filenames:
- return filename
+ if filename in self._filenames:
+ n = 1
+ basefilename, ext = splitext(filename)
- n = 1
- basefilename, ext = splitext(filename)
+ while True:
+ n += 1
+ filename = "%s-%d%s" % (basefilename, n, ext)
+ if filename not in self._filenames:
+ break
+ else:
+ self._filenames[filename] = uri
- while True:
- n += 1
- filename = "%s-%d%s" % (basefilename, n, ext)
- if filename not in self._filenames:
- return filename
+ return filename
- def add(self, value):
+ def add(self, value, replace=False):
if isinstance(value, self.file_cls):
uri = value.uri
@@ -75,24 +77,35 @@ class MemoryFileStore(FileStore):
else:
raise ValueError("Unknown file type: %s" % type(value))
- self.remove(uri)
- value.filename = self.unique_filename(value.filename)
- self._filenames.add(value.filename)
- self._files[uri] = value
+ if (uri not in self._files) or replace:
+ self.remove(uri)
+ value.filename = self.unique_filename(value.filename, uri=uri)
+ self._files[uri] = value
+ return value
- def by_uri(self, uri, synonims=None):
+ def by_uri(self, uri, synonyms=None):
r = self._files.get(uri, None)
if r:
return r
- if synonims:
- for _uri in synonims:
+ if synonyms:
+ for _uri in synonyms:
r = self._files.get(_uri, None)
if r:
return r
return None
+ def by_filename(self, filename):
+ uri = self._filenames.get(filename)
+ if uri:
+ return self.by_uri(uri)
+
+ def by_content_id(self, content_id):
+ parsed = self.file_cls.parse_content_id(content_id)
+ if parsed:
+ return self.by_filename(parsed['filename'])
+
def __getitem__(self, uri):
- return self._files.get(uri, None)
+ return self.by_uri(uri) or self.by_filename(uri)
def __iter__(self):
for k in self._files:
diff --git a/emails/testsuite/conftest.py b/emails/testsuite/conftest.py
index 581bd3b..5993e3f 100644
--- a/emails/testsuite/conftest.py
+++ b/emails/testsuite/conftest.py
@@ -7,7 +7,7 @@ import logging
import threading
import os
import os.path
-
+import datetime
import pytest
@@ -98,10 +98,82 @@ def smtp_server(request):
def django_email_backend(request):
from django.conf import settings
logger.debug('django_email_backend...')
- server = smtp_server(request)
- settings.configure(EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend',
- EMAIL_HOST=server.host, EMAIL_PORT=server.port)
+ settings.configure(EMAIL_BACKEND='django.core.mail.backends.filebased.EmailBackend',
+ EMAIL_FILE_PATH='tmp-emails')
from django.core.mail import get_connection
- SETTINGS = {}
return get_connection()
+
+class SMTPTestParams:
+
+ subject_prefix = '[test-python-emails]'
+
+ def __init__(self, from_email=None, to_email=None, defaults=None, **kw):
+ params = {}
+ params.update(defaults or {})
+ params.update(kw)
+ params['debug'] = 1
+ params['timeout'] = 15
+ self.params = params
+
+ self.from_email = from_email
+ self.to_email = to_email
+
+ def patch_message(self, message):
+ # Some SMTP requires from and to emails
+
+ if self.from_email:
+ message._mail_from = (message._mail_from[0], self.from_email)
+
+ if self.to_email:
+ message.mail_to = self.to_email
+
+ # TODO: this code breaks template in subject; deal with this
+ message.subject = " ".join([self.subject_prefix, datetime.datetime.now().strftime('%H:%M:%S'),
+ message.subject])
+
+ def __str__(self):
+ return u'SMTPTestParams(host={0}, port={1}, user={2})'.format(self.params.get('host'),
+ self.params.get('port'),
+ self.params.get('user'))
+
+
+
+@pytest.fixture(scope='module')
+def smtp_servers(request):
+
+ r = []
+
+ """
+ r.append(SMTPTestParams(from_email='drlavr@yandex.ru',
+ to_email='drlavr@yandex.ru',
+ fail_silently=False,
+ **{'host': 'mx.yandex.ru', 'port': 25, 'ssl': False}))
+
+ r.append(SMTPTestParams(from_email='drlavr+togmail@yandex.ru',
+ to_email='s.lavrinenko@gmail.com',
+ fail_silently=False,
+ **{'host': 'gmail-smtp-in.l.google.com', 'port': 25, 'ssl': False}))
+
+
+ r.append(SMTPTestParams(from_email='drlavr@yandex.ru',
+ to_email='s.lavrinenko@me.com',
+ fail_silently=False,
+ **{'host': 'mx3.mail.icloud.com', 'port': 25, 'ssl': False}))
+ """
+
+ r.append(SMTPTestParams(from_email='drlavr@yandex.ru',
+ to_email='lavr@outlook.com',
+ fail_silently=False,
+ **{'host': 'mx1.hotmail.com', 'port': 25, 'ssl': False}))
+
+ try:
+ from .local_smtp_settings import SMTP_SETTINGS_WITH_AUTH, FROM_EMAIL, TO_EMAIL
+ r.append(SMTPTestParams(from_email=FROM_EMAIL,
+ to_email=TO_EMAIL,
+ fail_silently=False,
+ **SMTP_SETTINGS_WITH_AUTH))
+ except ImportError:
+ pass
+
+ return r
\ No newline at end of file
diff --git a/emails/testsuite/smtp/test_django_integrations.py b/emails/testsuite/django_/test_django_integrations.py
similarity index 62%
rename from emails/testsuite/smtp/test_django_integrations.py
rename to emails/testsuite/django_/test_django_integrations.py
index 6f072d0..bfe1618 100644
--- a/emails/testsuite/smtp/test_django_integrations.py
+++ b/emails/testsuite/django_/test_django_integrations.py
@@ -1,6 +1,7 @@
# encoding: utf-8
from __future__ import unicode_literals
import emails
+import emails.message
def test_send_via_django_backend(django_email_backend):
@@ -9,7 +10,7 @@ def test_send_via_django_backend(django_email_backend):
Send email via django's email backend.
`django_email_backend` defined in conftest.py
"""
- message_params = {'html':'Test from python-emails',
+ message_params = {'html': '
Test from python-emails',
'mail_from': 's@lavr.me',
'mail_to': 's.lavrinenko@gmail.com',
'subject': 'Test from python-emails'}
@@ -22,3 +23,12 @@ def test_send_via_django_backend(django_email_backend):
headers = {'Reply-To': 'another@example.com'})
backend.send_messages([email, ])
+
+def test_django_message_proxy(django_email_backend):
+
+ message_params = {'html': '
Test from python-emails',
+ 'mail_from': 's@lavr.me',
+ 'mail_to': 's.lavrinenko@gmail.com',
+ 'subject': 'Test from python-emails'}
+ msg = emails.html(**message_params)
+ django_email_backend.send_messages([emails.message.DjangoMessageProxy(msg), ])
diff --git a/emails/testsuite/loader/data/html_import/oldornament.zip b/emails/testsuite/loader/data/html_import/oldornament.zip
index 1e3802dbe014870d3d8b25574894efe152f0780b..2e18f660b11aa49dfb439452f35f432dea05cd34 100644
GIT binary patch
delta 39716
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
delta 47814
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