python-emails refactored and redefined

This commit is contained in:
Sergey Lavrinenko 2015-02-21 23:56:59 +03:00
parent dd910dbb9a
commit c987129168
61 changed files with 1374 additions and 1795 deletions

8
.coveragerc Normal file
View File

@ -0,0 +1,8 @@
[run]
source = emails
[report]
omit =
emails/testsuite*
emails/packages*
emails/compat*

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
local_settings.py
local_*_settings.py
*.py[cod]
# C extensions

View File

@ -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"

10
Makefile Normal file
View File

@ -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

View File

@ -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('<p>Dear {{ name }}! This is a receipt for your subscription...'),
message = emails.html(subject=T('Payment Receipt No.{{ billno }}'),
html=T('<p>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="<img src='promo.png'>")
>>> message.transformer.apply_to_images(func=lambda src, **kw: 'http://mycompany.tld/images/'+src)
>>> message.transformer.save()
>>> message.html
u'<html><body><img src="http://mycompany.tld/images/promo.png"></body></html>'
Code example to make images inline:
::
>>> message = emails.Message(html="<img src='promo.png'>")
>>> 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'<html><body><img src="cid:promo.png"></body></html>'
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
--------

View File

@ -23,10 +23,12 @@ More examples is at <https://github.com/lavr/python-emails/README.rst>.
"""
__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

4
emails/exc.py Normal file
View File

@ -0,0 +1,4 @@
# encoding: utf-8
class HTTPLoaderError(Exception):
pass

View File

@ -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

View File

@ -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 <style> right into body
return view
def transform(self, html):
if isinstance(html, string_types):
html = etree.HTML(html, parser=etree.HTMLParser())
view = self.getView(html, self.stylesheet)
# - add style into @style attribute
for element, style in list(view.items()):
v = style.getCssText(separator='')
element.set('style', v)
return html
transform_html = transform # compatibility

View File

@ -6,6 +6,7 @@ import re
import cgi
import chardet
from emails.compat import to_unicode
import logging
# HTML page charset stuff
@ -25,7 +26,6 @@ def guess_charset(headers, html):
# guess by http headers
if headers:
#print(__name__, "guess_charset has headers", headers)
content_type = headers['content-type']
if content_type:
_, params = cgi.parse_header(content_type)
@ -34,7 +34,6 @@ def guess_charset(headers, html):
return r
# guess by html meta
#print(__name__, "guess_charset html=", html[:1024])
for s in RE_META.findall(html):
for x in RE_INSIDE_META.findall(s):
for charset in RE_CHARSET.findall(x):
@ -44,50 +43,3 @@ def guess_charset(headers, html):
return chardet.detect(html)['encoding']
def set_content_type_meta(document, element_cls, content_type="text/html", charset="utf-8"):
if document is None:
document = element_cls('html')
if document.tag!='html':
html = element_cls('html')
html.insert(0, document)
document = html
else:
html = document
head = document.find('head')
if head is None:
head = element_cls('head')
html.insert(0, head)
content_type_meta = None
for meta in head.find('meta') or []:
http_equiv = meta.get('http-equiv', None)
if http_equiv and (http_equiv.lower() == 'content_type'):
content_type_meta = meta
break
if content_type_meta is None:
content_type_meta = element_cls('meta')
head.append(content_type_meta)
content_type_meta.set('content', '%s; charset=%s' % (content_type, charset))
content_type_meta.set('http-equiv', "Content-Type")
return document
def add_body_stylesheet(document, element_cls, cssText, tag="body"):
style = element_cls('style')
style.text = cssText
body = document.find(tag)
if body is None:
body = document
body.insert(0, style)
return style

View File

@ -1,392 +0,0 @@
# encoding: utf-8
from __future__ import unicode_literals
import posixpath
import os.path
import logging
from lxml import etree
import requests
from emails.compat import urlparse, to_unicode, to_bytes, text_type
from emails.store import MemoryFileStore, LazyHTTPFile
from .stylesheets import PageStylesheets, StyledTagWrapper
from .cssinliner import CSSInliner
from .helpers import guess_charset
from .wrappers import TAG_WRAPPER, CSS_WRAPPER
from . import helpers
class HTTPLoaderError(Exception):
pass
class HTTPLoader:
"""
HTML loader loads single html page and store it as some sort of web archive:
* loads html page
* loads linked images
* loads linked css and images from css
* converts css to inline html styles
"""
USER_AGENT = 'python-emails/1.0'
UNSAFE_TAGS = set(['script', 'object', 'iframe', 'frame', 'base', 'meta', 'link', 'style'])
TAGS_WITH_BACKGROUND = set(['td', 'tr', 'th', 'body'])
TAGS_WITH_IMAGES = TAGS_WITH_BACKGROUND.union(set(['img', ]))
CSS_MEDIA = ['', 'screen', 'all', 'email']
tag_link_cls = {
'a': TAG_WRAPPER('href'),
'link': TAG_WRAPPER('href'),
'img': TAG_WRAPPER('src'),
'td': TAG_WRAPPER('background'),
'table': TAG_WRAPPER('background'),
'th': TAG_WRAPPER('background'),
}
css_link_cls = CSS_WRAPPER
attached_image_cls = LazyHTTPFile
filestore_cls = MemoryFileStore
def __init__(self, filestore=None, encoding='utf-8', fetch_params=None):
self.filestore = filestore or self.filestore_cls()
self.encoding = encoding
self.fetch_params = fetch_params
self.stylesheets = PageStylesheets()
self.base_url = None
self._attachments = None
self.local_loader = None
def _fetch(self, url, valid_http_codes=(200, ), fetch_params=None):
_params = dict(allow_redirects=True, verify=False,
headers={'User-Agent': self.USER_AGENT})
fetch_params = fetch_params or self.fetch_params
if fetch_params:
_params.update(fetch_params)
response = requests.get(url, **_params)
if valid_http_codes and (response.status_code not in valid_http_codes):
raise HTTPLoaderError('Error loading url: %s. HTTP status: %s' % (url, response.http_status))
return response
def get_html_tree(self):
return self._html_tree
def set_html_tree(self, value):
self._html_tree = value
self._html = None # We never actually store html, only cached html_tree render
html_tree = property(get_html_tree, set_html_tree)
def tag_has_link(self, tag):
return tag in self.tag_link_cls
def start_load_url(self, url, base_url=None):
"""
Set some params and load start page
"""
# Load start page
response = self._fetch(url, valid_http_codes=(200, ), fetch_params=self.fetch_params)
self.start_url = url
self.base_url = base_url or url # Fixme: split base_url
self.headers = response.headers
content = response.content
self.html_encoding = guess_charset(response.headers, content)
if self.html_encoding:
content = to_unicode(content, self.html_encoding)
else:
content = to_unicode(content)
content = content.replace('\r\n', '\n') # Remove \r, or we'll get &#13;
self.html_content = content
def start_load_file(self, html, encoding="utf-8"):
"""
Set some params and load start page
"""
if hasattr(html, 'read'):
html = html.read()
if not isinstance(html, text_type):
html = to_unicode(html, encoding)
html = html.replace('\r\n', '\n') # Remove \r, or we'll get &#13;
self.html_content = html
self.html_encoding = encoding
self.start_url = None
self.base_url = None
self.headers = None
def start_load_string(self, html, css):
self.html_content = html
if css:
self.stylesheets.append(text=css)
self.html_encoding = 'utf-8'
self.start_url = None
self.base_url = None
self.headers = None
def make_html_tree(self):
self.html_tree = etree.HTML(self.html_content, parser=etree.HTMLParser())
# TODO: try another load methods, i.e. etree.fromstring(xml,
# base_url="http://where.it/is/from.xml") ?
def parse_html_tree(self, remove_unsafe_tags=True):
# Parse html, load important tags
self._a_links = []
self._tags_with_links = []
self._tags_with_images = []
for el in self.html_tree.iter():
if el.tag == 'img' or el.tag == 'a' or self.tag_has_link(el.tag):
self.process_tag_with_link(el)
if el.tag == 'base':
self.base_url = el.get('href') # TODO: can be relative link in BASE HREF ?
elif el.tag == 'link':
self.process_external_css_tag(el)
elif el.tag == 'style':
self.process_style_tag(el)
# elif el.tag=='a':
# self.process_a_tag( el )
if el.get('style'):
self.process_tag_with_style(el)
if remove_unsafe_tags and (el.tag in self.UNSAFE_TAGS):
# Remove unsafe tags
# self._removed_unsafe.append(el) # Save it for reports
p = el.getparent()
if p is not None:
p.remove(el)
# now make concatenated stylesheet
for prop in self.stylesheets.uri_properties:
self.process_stylesheet_uri_property(prop)
self.attach_all_images()
def load_url(self, url, base_url=None, **kwargs):
self.start_load_url(url=url, base_url=base_url)
return self._load(**kwargs)
def load_file(self, file, local_loader=None, **kwargs):
self.local_loader = local_loader
self.start_load_file(html=file)
return self._load(**kwargs)
def load_string(self, html, css, **kwargs):
self.start_load_string(html=html, css=css)
return self._load(**kwargs)
def _load(self,
css_inline=True,
remove_unsafe_tags=True,
make_links_absolute=False,
set_content_type_meta=True,
update_stylesheet=True,
images_inline=False):
self.make_html_tree()
self.parse_html_tree(remove_unsafe_tags=remove_unsafe_tags)
if make_links_absolute:
[self.make_link_absolute(obj) for obj in self.iter_image_links()]
[self.make_link_absolute(obj) for obj in self.iter_a_links()]
if remove_unsafe_tags and update_stylesheet:
self.stylesheets.attach_tag(self.insert_big_stylesheet())
# self.process_attaches()
# TODO: process images in self._tags_with_styles
if css_inline:
self.doinlinecss()
if set_content_type_meta:
self.set_content_type_meta()
if images_inline:
self.make_images_inline()
def process_external_css_tag(self, el):
"""
Process <link href="..." rel="stylesheet">
"""
if el.get('rel', '') == 'stylesheet' and el.get('media', '') in self.CSS_MEDIA:
url = el.get('href', '')
if url:
self.stylesheets.append(url=url,
absolute_url=self.absolute_url(url),
local_loader=self.local_loader)
def process_style_tag(self, el):
"""
Process: <style>...</style>
"""
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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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', '<alice@me.com>' )
self._mail_from = mail_from and parse_name_and_email(mail_from) or None
def get_mail_from(self):
# Out: ('Alice', '<alice@me.com>') 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()

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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':'<p>Test from python-emails',
message_params = {'html': '<p>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': '<p>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), ])

View File

@ -1,152 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>SET-3-old-ornament</title>
</head>
<body style="background-color: #b7a98b; background-image: url(images/bg-all.jpg); color: #222121; font-family: 'Times New Roman', Times, serif; font-size: 13px; line-height: 16px; text-align: left;">
<table cellspacing="0" border="0" align="center" cellpadding="0" width="100%">
<tr>
<td valign="top">
<a name="top" style="text-decoration: none; color: #cc0000;"></a>
<table class="main-body" cellspacing="0" border="0" align="center" style="background-color: #d4c5a2; background-image: url(images/bg-main.jpg); font-family: 'Times New Roman', Times, serif;" cellpadding="0" width="616">
<tr>
<td class="unsubscribe" align="center" style="padding:20px 0"> <!-- unsubscribe -->
<p style="padding:0; margin: 0; font-family: 'Times New Roman', Times, serif; font-size: 12px;">You're receiving this newsletter because you bought widgets from us.<br />
Having trouble reading this email? <webversion style="color: #222121; text-decoration: underline;">View it in your browser</webversion>. Not interested anymore? <unsubscribe style="color: #222121; text-decoration: underline;">Unsubscribe</unsubscribe>.</p>
</td>
</tr>
<tr>
<td class="main-td" style="padding: 0 25px;"> <!-- introduction and menu box-->
<table class="intro" cellspacing="0" border="0" style="background-color: #e3ddca; background-image: url(images/bg-content.jpg); border-bottom: 1px solid #c3b697;" cellpadding="0" width="100%">
<tr>
<td valign="top" style="padding: 10px 12px 0px;" colspan="2">
<table class="banner" cellspacing="0" border="0" style="background: #550808; color: #fcfbfa;" cellpadding="0" width="100%">
<tr>
<td style="background: #e5ddca;"><img src="images/spacer.gif" height="2" style="display: block; border: none;" width="452" /></td>
<td align="right" style="background: #e5ddca;"><img src="images/banner-top.gif" height="2" style="display: block; border: none;" width="90" /></td>
</tr>
<tr>
<td class="title" valign="top" style="padding: 0 12px 0;">
<img src="images/spacer.gif" width="1" height="35" style="display: block; border: none;">
<h1 style="padding: 0; color:#fcfbfa; font-family: 'Times New Roman', Times, serif; font-size: 60px; line-height: 60px; margin: 0;">ABC Widgets</h1>
<p style="padding: 0; color:#fcfbfa; font-family: 'Times New Roman', Times, serif; font-size: 16px; text-transform: uppercase; margin: 0;"><currentmonthname> NEWSLETTER</p>
</td>
<td valign="top" align="right" width="90"><img src="images/banner-middle.gif" height="144" style="display: block; border: none;" width="90" /></td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="content" align="left" valign="top" style="font-size: 15px; font-style: italic; line-height: 18px; padding:0 35px 12px 12px; width: 329px;">
<table width="100%" border="0" cellspacing="0" cellpadding="0" style=" font-family: 'Times New Roman', Times, serif;">
<tr>
<td style="padding:25px 0 0;">
<p style="padding:0; font-family: 'Times New Roman', Times, serif;"><strong>Dear Simon,</strong></p>
<p style="padding:0; font-family: 'Times New Roman', Times, serif;">Welcome to lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam molestie quam vitae mi congue tristique. Aliquam lectus orci, adipiscing et, sodales ac. Ut dictum velit nec est. Quisque posuere, purus sit amet malesuada blandit, sapien sapien.</p>
<p style="padding:0; font-family: 'Times New Roman', Times, serif;">Regards, ABC Widgets</p>
</td>
</tr>
</table>
</td>
<td class="menu" align="left" valign="top" style="width: 178px; padding: 0 12px 0 0;">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td valign="top" align="right"><img src="images/banner-bottom.png" height="55" style="display: block; border: none;" width="178" /></td>
</tr>
<tr>
<td valign="top" align="left">
<ul style="margin: 0; padding: 0;">
<li style="font-size: 12px; font-family: 'Times New Roman', Times, serif; text-transform: uppercase; border-bottom: 1px solid #c0bcb1; color: #222121; list-style-type: none; padding: 5px 0; display:block">in this issue</li>
<li style=" font-family: 'Times New Roman', Times, serif; list-style-type: none; padding: 5px 0; border-bottom: 1px solid #c0bcb1; display:block"><a href="#article1" style="text-decoration: none; color: #cc0000;">Lorem ipsum dolor sit amet</a></li>
<li style=" font-family: 'Times New Roman', Times, serif; list-style-type: none; padding: 5px 0; border-bottom: 1px solid #c0bcb1; display:block"><a href="#article2" style="text-decoration: none; color: #cc0000;">Consectetuer adipiscing elit</a></li>
<li style=" font-family: 'Times New Roman', Times, serif; list-style-type: none; padding: 5px 0; border-bottom: 1px solid #c0bcb1; display:block"><a href="#article3" style="text-decoration: none; color: #cc0000;">Aliquam molestie quam vitae</a></li>
<li style=" font-family: 'Times New Roman', Times, serif; list-style-type: none; padding: 5px 0; border-bottom: 1px solid #c0bcb1; display:block"><a href="#article4" style="text-decoration: none; color: #cc0000;">Congue tristique</a></li>
</ul>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="footer" valign="top" colspan="2"><img src="images/spacer.gif" height="15" style="display: block; border: none;" width="1" /></td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="flourish" valign="top" style="padding: 22px 25px;"><img src="images/flourish.png" height="35" style="display: block; border: none;" width="566" /></td>
</tr>
<tr>
<td align="left" valign="top" style="padding: 0 25px;">
<table cellspacing="0" cellpadding="0" border="0" width="100%" style=" font-family: 'Times New Roman', Times, serif; font-size:13px; color: #222121;">
<tr>
<!-- main content -->
<td align="left" valign="top" style="background-color:#e3dcc9; background-image: url(images/bg-content.jpg); padding: 13px;">
<p class="title" style="font-family: 'Times New Roman', Times, serif; padding: 8px 0; font-size: 18px; color: #ab1212; margin: 0;">Lorem Ipsum Dolor Sit Amet</p><a name="article1"></a>
<p style="padding:0; margin:0 0 13px"><img class="divider" src="images/divider.jpg" height="5" style="display: block; border: none;" width="332" /></p>
<table cellspacing="0" border="0" style="background: #f0ece2; border: 1px solid #d5d2c9;" cellpadding="0" width="100%">
<tr>
<td style="padding: 11px;"><img src="images/img01.jpg" height="181" style="display: block; border: none;" width="311" /></td>
</tr>
</table>
<p style="font-family: 'Times New Roman', Times, serif; padding: 0; margin:13px 0;">Cras purus. Nunc rhoncus. Pellentesque semper. Donec imperdiet accumsan felis. Proin eget mi. Sed at est. Aliquam lectus orci, adipiscing et, sodales ac celeste. Ut dictum velit nec est. Quisque posuere, purus sit amet malesuada blandit, sapien sapien auctor arcu</p>
<p class="title" style="font-family: 'Times New Roman', Times, serif; padding: 8px 0; font-size: 18px; color: #ab1212; margin: 0;">Fermentum Quam Etur Lectus</p><a name="article2"></a>
<p style="padding:0; margin:0 0 13px"><img class="divider" src="images/divider.jpg" height="5" style="display: block; border: none;" width="332" /></p>
<table cellspacing="0" border="0" style="background: #f0ece2; border: 1px solid #d5d2c9;" cellpadding="0" width="100%">
<tr>
<td style="padding: 11px;"><img src="images/img02.jpg" height="181" style="display: block; border: none;" width="311" /></td>
</tr>
</table>
<p style="font-family: 'Times New Roman', Times, serif; padding: 0; margin:13px 0;">Suspendisse potenti--Fusce eu ante in sapien vestibulum sagittis. Cras purus. Nunc rhoncus. Donec imperdiet, nibh sit amet pharetra placerat, tortor purus condimentum lectus, at dignissim nibh velit vitae sem. Nunc condimentum blandit tortorphasellus neque vitae purus.</p>
<p class="title" style=" font-family: 'Times New Roman', Times, serif; padding: 8px 0; font-size: 18px; color: #ab1212; margin: 0;">Lorem Ipsum Dolor Sit Amet</p><a name="article3"></a>
<p style="padding:0; margin:0 0 13px"><img class="divider" src="images/divider.jpg" height="5" style="display: block; border: none;" width="332" /></p>
<table cellspacing="0" border="0" style="background: #f0ece2; border: 1px solid #d5d2c9;" cellpadding="0" width="100%">
<tr>
<td style="padding: 11px;"><img src="images/img03.jpg" height="181" style="display: block; border: none;" width="311" /></td>
</tr>
</table>
<p style=" font-family: 'Times New Roman', Times, serif; padding: 0; margin:13px 0;">Cras purus. Nunc rhoncus. Pellentesque semper. Donec imperdiet accumsan felis. Proin eget mi. Sed at est. Aliquam lectus orci, adipiscing et, sodales ac celeste. Ut dictum velit nec est. Quisque posuere, purus sit amet malesuada blandit, sapien sapien auctor arcu</p>
</td>
<!-- sidebar -->
<td align="left" valign="top" style="background-color:#e9e3d6; padding:13px 12px 13px 17px;">
<p style="padding:8px 0 8px; font-family: 'Times New Roman', Times, serif; font-size:18px; color:#222121; margin:0;">Forward this issue</p>
<p style="padding:0; margin:0 0 13px"><img class="divider" src="images/divider2.jpg" height="5" style="display: block; border: none;" width="178" /></p>
<p style=" font-family: 'Times New Roman', Times, serif; margin: 0 0 13px; padding: 0;">Do you know someone who might be interested in receiving this monthly newsletter?</p>
<p style=" font-family: 'Times New Roman', Times, serif; padding: 0;"><forwardtoafriend style="text-transform: uppercase; color: #cc0000; text-decoration: none;"><strong>forward</strong></forwardtoafriend></p>
<p style="padding:34px 0 8px; font-family: 'Times New Roman', Times, serif; font-size:18px; color:#222121; margin:0;">Unsubscribe</p>
<p style="padding:0; margin:0 0 13px"><img class="divider" src="images/divider2.jpg" height="5" style="display: block; border: none;" width="178" /></p>
<p style=" font-family: 'Times New Roman', Times, serif; margin: 0 0 13px; padding: 0;">You're receiving this newsletter because you signed up for the ABC Widget Newsletter.</p>
<p style=" font-family: 'Times New Roman', Times, serif; padding: 0;"><unsubscribe style="text-transform: uppercase; color: #cc0000; text-decoration: none;"><strong>unsubscribe</strong></unsubscribe></p>
<p style="padding:34px 0 8px; font-family: 'Times New Roman', Times, serif; font-size:18px; color:#222121; margin:0;">Contact us</p>
<p style="padding:0; margin:0 0 13px"><img class="divider" src="images/divider2.jpg" height="5" style="display: block; border: none;" width="178" /></p>
<p style=" font-family: 'Times New Roman', Times, serif; margin: 0; padding: 0;">123 Some Street<br />
City, State<br />
99999<br />
(147) 789 7745<br />
<a href="#" style="text-decoration: none; color: #cc0000;">www.abcwidgets.com</a><br />
<a href="mailto:info@abcwidgets.com" style="text-decoration: none; color: #cc0000;">info@abcwidgets.com</a></p>
</td>
</tr>
<tr>
<!-- back to top -->
<td align="left" valign="top" style=" font-family: 'Times New Roman', Times, serif; background: #dfd8c8; padding: 10px 0 10px 14px;" colspan="2"><a href="#top" style="text-decoration: none; color: #cc0000;"><strong>Back to top</strong></a></td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left" valign="top" style="padding: 22px 25px;"><img src="images/flourish.png" height="35" style="display: block; border: none;" width="566" /></td>
</tr>
</table>
</td>
</tr>
</table>
</body>

View File

@ -1,153 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>SET-3-old-ornament</title>
</head>
<body style="background-color: #b7a98b; background-image: url(images/bg-all.jpg); color: #222121; font-family: 'Times New Roman', Times, serif; font-size: 13px; line-height: 16px; text-align: left;">
<table cellspacing="0" border="0" align="center" cellpadding="0" width="100%">
<tr>
<td valign="top">
<a name="top" style="text-decoration: none; color: #cc0000;"></a>
<table class="main-body" cellspacing="0" border="0" align="center" style="background-color: #d4c5a2; background-image: url(images/bg-main.jpg);" cellpadding="0" width="616">
<tr>
<td class="unsubscribe" align="center" style="padding:20px 0"> <!-- unsubscribe -->
<p style="padding:0; margin: 0; font-family: 'Times New Roman', Times, serif; font-size: 12px;">You're receiving this newsletter because you bought widgets from us.<br />
Having trouble reading this email? <webversion style="color: #222121; text-decoration: underline;">View it in your browser</webversion>. Not interested anymore? <unsubscribe style="color: #222121; text-decoration: underline;">Unsubscribe</unsubscribe>.</p>
</td>
</tr>
<tr>
<td class="main-td" style="padding: 0 25px;"> <!-- introduction and menu box-->
<table class="intro" cellspacing="0" border="0" style="background-color: #e3ddca; background-image: url(images/bg-content.jpg); border-bottom: 1px solid #c3b697;" cellpadding="0" width="100%">
<tr>
<td valign="top" style="padding: 10px 12px 0px;" colspan="2">
<table class="banner" cellspacing="0" border="0" style="background: #550808; color: #fcfbfa;" cellpadding="0" width="100%">
<tr>
<td style="background: #e5ddca;"><img src="images/spacer.gif" height="2" style="display: block; border: none;" width="452" /></td>
<td align="right" style="background: #e5ddca;"><img src="images/banner-top.gif" height="2" style="display: block; border: none;" width="90" /></td>
</tr>
<tr>
<td class="title" valign="top" style="padding: 0 12px 0;">
<img src="images/spacer.gif" width="1" height="35" style="display: block; border: none;">
<h1 style="padding: 0; color:#fcfbfa; font-family: 'Times New Roman', Times, serif; font-size: 60px; line-height: 60px; margin: 0;">ABC Widgets</h1>
<p style="padding: 0; color:#fcfbfa; font-family: 'Times New Roman', Times, serif; font-size: 16px; text-transform: uppercase; margin: 0;"><currentmonthname> NEWSLETTER</p>
</td>
<td valign="top" align="right" width="90"><img src="images/banner-middle.gif" height="144" style="display: block; border: none;" width="90" /></td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="content" align="left" valign="top" style="font-size: 15px; font-style: italic; line-height: 18px; padding:0 35px 12px 12px; width: 329px;">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td style="padding:25px 0 0;">
<p style=" font-family: 'Times New Roman', Times, serif; padding:0;"><strong>Dear Simon,</strong></p>
<p style=" font-family: 'Times New Roman', Times, serif; padding:0;">Welcome to lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam molestie quam vitae mi congue tristique. Aliquam lectus orci, adipiscing et, sodales ac. Ut dictum velit nec est. Quisque posuere, purus sit amet malesuada blandit, sapien sapien.</p>
<p style="font-family: 'Times New Roman', Times, serif; padding:0;">Regards, ABC Widgets</p>
</td>
</tr>
</table>
</td>
<td class="menu" align="left" valign="top" style="width: 178px; padding: 0 12px 0 0;">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td valign="top" align="right"><img src="images/banner-bottom.png" height="55" style="display: block; border: none;" width="178" /></td>
</tr>
<tr>
<td valign="top" align="left">
<ul style="margin: 0; padding: 0;">
<li style="font-family: 'Times New Roman', Times, serif; font-size: 12px; text-transform: uppercase; border-bottom: 1px solid #c0bcb1; color: #222121; list-style-type: none; padding: 5px 0; display:block">in this issue</li>
<li style="font-family: 'Times New Roman', Times, serif; list-style-type: none; padding: 5px 0; border-bottom: 1px solid #c0bcb1; display:block"><a href="#article1" style="text-decoration: none; color: #cc0000;">Lorem ipsum dolor sit amet</a></li>
<li style="font-family: 'Times New Roman', Times, serif; list-style-type: none; padding: 5px 0; border-bottom: 1px solid #c0bcb1; display:block"><a href="#article2" style="text-decoration: none; color: #cc0000;">Consectetuer adipiscing elit</a></li>
<li style="font-family: 'Times New Roman', Times, serif; list-style-type: none; padding: 5px 0; border-bottom: 1px solid #c0bcb1; display:block"><a href="#article3" style="text-decoration: none; color: #cc0000;">Aliquam molestie quam vitae</a></li>
<li style="font-family: 'Times New Roman', Times, serif; list-style-type: none; padding: 5px 0; border-bottom: 1px solid #c0bcb1; display:block"><a href="#article4" style="text-decoration: none; color: #cc0000;">Congue tristique</a></li>
</ul>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="footer" valign="top" colspan="2"><img src="images/spacer.gif" height="15" style="display: block; border: none;" width="1" /></td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="flourish" valign="top" style="padding: 22px 25px;"><img src="images/flourish.png" height="35" style="display: block; border: none;" width="566" /></td>
</tr>
<tr>
<td align="left" valign="top" style="padding: 0 25px;">
<table cellspacing="0" cellpadding="0" border="0" width="100%" >
<tr>
<!-- sidebar -->
<td align="left" valign="top" style="background-color:#e9e3d6; padding:13px 12px 13px 17px;">
<p style="padding:8px 0 8px; font-family: 'Times New Roman', Times, serif; font-size:18px; color:#222121; margin:0;">Forward this issue</p>
<p style="padding:0; margin:0 0 13px"><img class="divider" src="images/divider2.jpg" height="5" style="display: block; border: none;" width="178" /></p>
<p style=" font-family: 'Times New Roman', Times, serif; margin: 0; padding: 0;">Do you know someone who might be interested in receiving this monthly newsletter?</p>
<p style=" font-family: 'Times New Roman', Times, serif; padding: 0;"><forwardtoafriend style="text-transform: uppercase; color: #cc0000; text-decoration: none;"><strong>forward</strong></forwardtoafriend></p>
<p style="padding:34px 0 8px; font-family: 'Times New Roman', Times, serif; font-size:18px; color:#222121; margin:0;">Unsubscribe</p>
<p style="padding:0; margin:0 0 13px"><img class="divider" src="images/divider2.jpg" height="5" style="display: block; border: none;" width="178" /></p>
<p style=" font-family: 'Times New Roman', Times, serif; margin: 0; padding: 0;">You're receiving this newsletter because you signed up for the ABC Widget Newsletter.</p>
<p style=" font-family: 'Times New Roman', Times, serif; padding: 0;"><unsubscribe style="text-transform: uppercase; color: #cc0000; text-decoration: none;"><strong>unsubscribe</strong></unsubscribe></p>
<p style="padding:34px 0 8px; font-family: 'Times New Roman', Times, serif; font-size:18px; color:#222121; margin:0;">Contact us</p>
<p style="padding:0; margin:0 0 13px"><img class="divider" src="images/divider2.jpg" height="5" style="display: block; border: none;" width="178" /></p>
<p style=" font-family: 'Times New Roman', Times, serif; margin: 0; padding: 0;">123 Some Street<br />
City, State<br />
99999<br />
(147) 789 7745<br />
<a href="#" style="text-decoration: none; color: #cc0000;">www.abcwidgets.com</a><br />
<a href="mailto:info@abcwidgets.com" style="text-decoration: none; color: #cc0000;">info@abcwidgets.com</a></p>
</td>
<!-- main content -->
<td align="left" valign="top" style="background-color:#e3dcc9; background-image: url(images/bg-content.jpg); padding: 13px;">
<p class="title" style="padding: 8px 0; font-family: 'Times New Roman', Times, serif; font-size: 18px; color: #ab1212; margin: 0;">Lorem Ipsum Dolor Sit Amet</p><a name="article1"></a>
<p style="padding:0; margin:0 0 13px"><img class="divider" src="images/divider.jpg" height="5" style="display: block; border: none;" width="332" /></p>
<table cellspacing="0" border="0" style="background: #f0ece2; border: 1px solid #d5d2c9;" cellpadding="0" width="100%">
<tr>
<td style="padding: 11px;"><img src="images/img01.jpg" height="181" style="display: block; border: none;" width="311" /></td>
</tr>
</table>
<p style=" font-family: 'Times New Roman', Times, serif; padding: 0;">Cras purus. Nunc rhoncus. Pellentesque semper. Donec imperdiet accumsan felis. Proin eget mi. Sed at est. Aliquam lectus orci, adipiscing et, sodales ac celeste. Ut dictum velit nec est. Quisque posuere, purus sit amet malesuada blandit, sapien sapien auctor arcu</p>
<p class="title" style="padding: 8px 0; font-family: 'Times New Roman', Times, serif; font-size: 18px; color: #ab1212; margin: 0;">Fermentum Quam Etur Lectus</p><a name="article2"></a>
<p style="padding:0; margin:0 0 13px"><img class="divider" src="images/divider.jpg" height="5" style="display: block; border: none;" width="332" /></p>
<table cellspacing="0" border="0" style="background: #f0ece2; border: 1px solid #d5d2c9;" cellpadding="0" width="100%">
<tr>
<td style="padding: 11px;"><img src="images/img02.jpg" height="181" style="display: block; border: none;" width="311" /></td>
</tr>
</table>
<p style=" font-family: 'Times New Roman', Times, serif; padding: 0;">Suspendisse potenti--Fusce eu ante in sapien vestibulum sagittis. Cras purus. Nunc rhoncus. Donec imperdiet, nibh sit amet pharetra placerat, tortor purus condimentum lectus, at dignissim nibh velit vitae sem. Nunc condimentum blandit tortorphasellus neque vitae purus.</p>
<p class="title" style="padding: 8px 0; font-family: 'Times New Roman', Times, serif; font-size: 18px; color: #ab1212; margin: 0;">Lorem Ipsum Dolor Sit Amet</p><a name="article3"></a>
<p style="padding:0; margin:0 0 13px"><img class="divider" src="images/divider.jpg" height="5" style="display: block; border: none;" width="332" /></p>
<table cellspacing="0" border="0" style="background: #f0ece2; border: 1px solid #d5d2c9;" cellpadding="0" width="100%">
<tr>
<td style="padding: 11px;"><img src="images/img03.jpg" height="181" style="display: block; border: none;" width="311" /></td>
</tr>
</table>
<p style=" font-family: 'Times New Roman', Times, serif; padding: 0;">Cras purus. Nunc rhoncus. Pellentesque semper. Donec imperdiet accumsan felis. Proin eget mi. Sed at est. Aliquam lectus orci, adipiscing et, sodales ac celeste. Ut dictum velit nec est. Quisque posuere, purus sit amet malesuada blandit, sapien sapien auctor arcu</p>
</td>
</tr>
<tr>
<td style="background-color:#e2dbcd;">&nbsp;</td>
<!-- back to top -->
<td align="left" valign="top" style="background: #dfd8c8; padding: 10px 0 10px 14px;"><a href="#top" style="text-decoration: none; font-family: 'Times New Roman', Times, serif; color: #cc0000;"><strong>Back to top</strong></a></td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left" valign="top" style="padding: 22px 25px;"><img src="images/flourish.png" height="35" style="display: block; border: none;" width="566" /></td>
</tr>
</table>
</td>
</tr>
</table>
</body>

View File

@ -156,7 +156,7 @@
City, State<br />
99999<br />
(147) 789 7745<br />
<a href="#" style="text-decoration: none; color: #cc0000;">www.abcwidgets.com</a><br />
<a href="" style="text-decoration: none; color: #cc0000;">www.abcwidgets.com</a><br />
<a href="mailto:info@abcwidgets.com" style="text-decoration: none; color: #cc0000;">info@abcwidgets.com</a></p>
</td>
</tr>

View File

@ -1,208 +0,0 @@
# encoding: utf-8
from __future__ import unicode_literals
import emails
from emails.loader.stylesheets import StyledTagWrapper
from emails.compat import to_unicode
import lxml
import lxml.etree
import os.path
def test_tagwithstyle():
content = """<div style="background: url('http://yandex.ru/bg.png'); color: black;"/>"""
tree = lxml.etree.HTML(content, parser=lxml.etree.HTMLParser())
t = None
for el in tree.iter():
if el.get('style'):
t = StyledTagWrapper(el)
assert len(list(t.uri_properties())) == 1
def normalize_html(s):
return "".join(to_unicode(s).split())
def test_insert_style():
html = """ <img src="1.png" style="background: url(2.png)"> <style>p {background: url(3.png)} </style> """
tree = lxml.etree.HTML(html, parser=lxml.etree.HTMLParser())
# print __name__, "test_insert_style step1: ", lxml.etree.tostring(tree, encoding='utf-8', method='html')
emails.loader.helpers.add_body_stylesheet(tree,
element_cls=lxml.etree.Element,
tag="body",
cssText="")
#print __name__, "test_insert_style step2: ", lxml.etree.tostring(tree, encoding='utf-8', method='html')
new_document = emails.loader.helpers.set_content_type_meta(tree, element_cls=lxml.etree.Element)
if tree != new_document:
# document may be updated here (i.e. html tag added)
tree = new_document
html = normalize_html(lxml.etree.tostring(tree, encoding='utf-8', method='html'))
RESULT_HTML = normalize_html(
'<html><head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head><body>'
'<style></style><img src="1.png" style="background: url(2.png)"> '
'<style>p {background: url(3.png)} </style> </body></html>')
assert html == RESULT_HTML, "Invalid html expected: %s, got: %s" % (RESULT_HTML.__repr__(), html.__repr__())
def test_all_images():
# Check if we load images from CSS:
styles = emails.loader.stylesheets.PageStylesheets()
styles.append(text="p {background: url(3.png);}")
assert len(styles.uri_properties) == 1
# Check if we load all images from html:
HTML1 = """ <img src="1.png" style="background: url(2.png)"> <style>p {background: url(3.png)} </style> """
loader = emails.loader.from_string(html=HTML1)
# should be 3 image_link object
assert len(list(loader.iter_image_links())) == 3
# should be 3 files in filestore
files = set(loader.filestore.keys())
assert len(files) == 3
# Check if changing links affects result html:
for obj in loader.iter_image_links():
obj.link = "prefix_" + obj.link
result_html = normalize_html(loader.html)
VALID_RESULT = normalize_html("""<html><head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>"""
"""</head><body><style>p { background: url(prefix_3.png) }</style>"""
"""<img src="prefix_1.png" style="background: url(prefix_2.png)"/> </body></html>""")
assert result_html == VALID_RESULT, "Invalid html expected: %s, got: %s" % (
result_html.__repr__(), VALID_RESULT.__repr__())
def test_load_local_directory():
ROOT = os.path.dirname(__file__)
colordirect_html = "data/html_import/colordirect/html/left_sidebar.html"
colordirect_loader = emails.loader.from_file(os.path.join(ROOT, colordirect_html))
ALL_FILES = "bg_divider_top.png,bullet.png,img.png,img_deco_bottom.png,img_email.png," \
"bg_email.png,ico_lupa.png,img_deco.png".split(',')
ALL_FILES = set(["images/" + n for n in ALL_FILES])
files = set(colordirect_loader.filestore.keys())
not_attached = ALL_FILES - files
assert len(not_attached) == 0, "Not attached files found: %s" % not_attached
for fn in ( "data/html_import/colordirect/html/full_width.html",
"data/html_import/oldornament/html/full_width.html"
):
filename = os.path.join(ROOT, fn)
print(fn)
loader = emails.loader.from_file(filename)
print(loader.html)
def test_load_http():
URLs = [
'http://lavr.github.io/python-emails/tests/campaignmonitor-samples/sample-template/template-widgets.html',
'https://github.com/lavr/python-emails',
'http://cnn.com',
'http://yandex.com',
'http://yahoo.com',
'http://www.smashingmagazine.com/'
]
for url in URLs[:1]:
# Load some sites.
# Loader just shouldn't throw exception
emails.loader.from_url(url)
def test_load_zip():
ROOT = os.path.dirname(__file__)
filename = os.path.join(ROOT, "data/html_import/oldornament.zip")
loader = emails.loader.from_zip(open(filename, 'rb'))
assert len(list(loader.filestore.keys())) >= 13
assert "SET-3-old-ornament" in loader.html
def _do_inline_css(html, css, save_to_file=None, pretty_print=False):
inliner = emails.loader.cssinliner.CSSInliner()
inliner.DEBUG = True
inliner.add_css(css)
document = inliner.transform_html(html)
r = lxml.etree.tostring(document, pretty_print=pretty_print)
if save_to_file:
open(save_to_file, 'wb').write(r)
return r
def test_unmergeable_css():
HTML = "<a>b</a>"
CSS = "a:visited {color: red;}"
r = _do_inline_css(HTML, CSS) # , save_to_file='_result.html')
print(r)
def test_commons_css_inline():
tmpl = '''<html><head><title>style test</title></head><body>%s</body></html>'''
HTML = tmpl % '''
<h1>Style example 1</h1>
<p>&lt;p></p>
<p style="color: red;">&lt;p> with inline style: "color: red"</p>
<p id="x" style="color: red;">p#x with inline style: "color: red"</p>
<div>a &lt;div> green?</div>
<div id="y">#y pink?</div>
'''
CSS = r'''
* {
margin: 0;
}
body {
color: blue !important;
font: normal 100% sans-serif;
}
p {
c\olor: green;
font-size: 2em;
}
p#x {
color: black !important;
}
div {
color: green;
font-size: 1.5em;
}
#y {
color: #f0f;
}
.cssutils {
font: 1em "Lucida Console", monospace;
border: 1px outset;
padding: 5px;
}
'''
VALID_RESULT = normalize_html("""<html>
<head>
<title>style test</title>
</head>
<body style="margin: 0;color: blue !important;font: normal 100% sans-serif">
<h1 style="margin: 0">Style example 1</h1>
<p style="margin: 0;color: green;font-size: 2em">&lt;p&gt;</p>
<p style="color: red;margin: 0;font-size: 2em">&lt;p&gt; with inline style: "color: red"</p>
<p id="x" style="color: black !important;margin: 0;font-size: 2em">p#x with inline style: "color: red"</p>
<div style="margin: 0;color: green;font-size: 1.5em">a &lt;div&gt; green?</div>
<div id="y" style="margin: 0;color: #f0f;font-size: 1.5em">#y pink?</div>
</body>
</html>""")
result = normalize_html(_do_inline_css(HTML, CSS, pretty_print=True)) # , save_to_file='_result.html')
assert VALID_RESULT.strip() == result.strip(), "Invalid html got: %s, expected: %s" % (
result.__repr__(), VALID_RESULT.__repr__())

View File

@ -0,0 +1,106 @@
# encoding: utf-8
from __future__ import unicode_literals
import glob
import os.path
import email
from requests import ConnectionError
import emails
import emails.loader
import emails.transformer
from emails.loader.local_store import MsgLoader
ROOT = os.path.dirname(__file__)
BASE_URL = 'http://lavr.github.io/python-emails/tests/campaignmonitor-samples/oldornament'
def _get_messages(**kw):
# All loaders loads same data
yield emails.loader.from_url(BASE_URL + '/index.html', **kw)
yield emails.loader.from_file(os.path.join(ROOT, "data/html_import/oldornament/index.html"), **kw)
yield emails.loader.from_zip(open(os.path.join(ROOT, "data/html_import/oldornament.zip"), 'rb'), **kw)
def normalize_html(s):
def _remove_base_url(src, **kw):
if src.startswith(BASE_URL):
return src[len(BASE_URL)+1:]
else:
return src
# Use Transformer not for test, just to walk tree
t = emails.transformer.Transformer(html=s)
t.apply_to_links(_remove_base_url)
t.apply_to_images(_remove_base_url)
return t.to_string()
def all_equals(seq):
iseq = iter(seq)
first = next(iseq)
return all(x == first for x in iseq)
def test_loaders():
messages = list(_get_messages())
# Check loaded images
for m in messages:
assert len(m.attachments.keys()) == 13
valid_filenames = ['arrow.png', 'banner-bottom.png', 'banner-middle.gif', 'banner-top.gif', 'bg-all.jpg',
'bg-content.jpg', 'bg-main.jpg', 'divider.jpg', 'flourish.png', 'img01.jpg', 'img02.jpg',
'img03.jpg', 'spacer.gif']
assert sorted([a.filename for a in messages[0].attachments]) == sorted(valid_filenames)
assert len(messages[0].attachments.by_filename('arrow.png').data) == 484
# Simple html content check
htmls = [normalize_html(m.html) for m in messages]
assert 'Lorem Ipsum Dolor Sit Amet' in htmls[0]
assert all_equals(htmls)
def _test_external_urls():
# Load some real sites with complicated html and css.
# Test loader don't throw any exception.
for url in [
'https://github.com/lavr/python-emails',
'http://yandex.com',
'http://www.smashingmagazine.com/'
]:
try:
emails.loader.from_url(url)
except ConnectionError:
# Nevermind if external site does not respond
pass
def test_msgloader():
data = {'charset': 'utf-8',
'subject': 'Что-то по-русски',
'mail_from': ('Максим Иванов', 'ivanov@ya.ru'),
'mail_to': ('Полина Сергеева', 'polina@mail.ru'),
'html': '<h1>Привет!</h1><p>В первых строках...',
'text': 'Привет!\nВ первых строках...',
'headers': {'X-Mailer': 'python-emails'},
'attachments': [{'data': 'aaa', 'filename': 'Event.ics'},],
'message_id': 'message_id'}
msg = emails.Message(**data).as_string()
loader = MsgLoader(msg=msg)
loader._parse_msg()
assert 'Event.ics' in loader.list_files()
assert loader['__index.html'] == data['html']
assert loader['__index.txt'] == data['text']
def _test_mass_msgloader():
ROOT = os.path.dirname(__file__)
for filename in glob.glob(os.path.join(ROOT, "data/msg/*.eml")):
msg = email.message_from_string(open(filename).read())
msgloader = MsgLoader(msg=msg)
msgloader._parse_msg()

View File

@ -1,49 +1,31 @@
# coding: utf-8
from __future__ import unicode_literals
import logging
import os
from emails.loader import cssinliner
import emails
from emails.compat import StringIO
from emails.template import JinjaTemplate
from emails.compat import NativeStringIO, to_bytes
TO_EMAIL = 'jbrown@hotmail.tld'
FROM_EMAIL = 'robot@company.tld'
TRAVIS_CI = os.environ.get('TRAVIS')
HAS_INTERNET_CONNECTION = not TRAVIS_CI
def common_email_data(**kwargs):
data = {'charset': 'utf-8',
'subject': 'Что-то по-русски',
'mail_from': ('Максим Иванов', 'ivanov@ya.ru'),
'mail_to': ('Полина Сергеева', 'polina@mail.ru'),
'html': '<h1>Привет!</h1><p>В первых строках...',
'text': 'Привет!\nВ первых строках...',
'headers': {'X-Mailer': 'python-emails'},
'attachments': [{'data': 'aaa', 'filename': 'Event.ics'},
{'data': StringIO('bbb'), 'filename': 'map.png'}],
'message_id': emails.MessageID()}
if kwargs:
data.update(kwargs)
return data
def _email_data(**kwargs):
def common_email_data(**kw):
T = JinjaTemplate
data = {'charset': 'utf-8',
'subject': T('Hello, {{name}}'),
'mail_from': ('Максим Иванов', 'sergei-nko@mail.ru'),
'mail_to': ('Полина Сергеева', 'sergei-nko@mail.ru'),
'html': T('<h1>Привет, {{name}}!</h1><p>В первых строках...'),
'text': T('Привет, {{name}}!\nВ первых строках...'),
'subject': T('[python-emails test] Olá {{name}}'),
'mail_from': ('LÖVÅS HÅVET', FROM_EMAIL),
'mail_to': ('Pestävä erillään', TO_EMAIL),
'html': T('<h1>Olá {{name}}!</h1><p>O Lorem Ipsum é um texto modelo da indústria tipográfica e de impressão.'),
'text': T('Olá, {{name}}!\nO Lorem Ipsum é um texto modelo da indústria tipográfica e de impressão.'),
'headers': {'X-Mailer': 'python-emails'},
'message_id': emails.MessageID(),
'attachments': [
{'data': 'aaa', 'filename': 'Event.ics', 'content_disposition': 'attachment'},
{'data': 'bbb', 'filename': 'Карта.png', 'content_disposition': 'attachment'}
{'data': 'aaa', 'filename': 'κατάσχεση.ics'},
{'data': 'bbb', 'filename': 'map.png'}
]}
if kwargs:
data.update(kwargs)
return data
if kw:
data.update(kw)
return data

View File

@ -1,13 +1,7 @@
# coding: utf-8
from __future__ import unicode_literals
import logging
import os
from emails.loader import cssinliner
import emails
from emails.compat import StringIO
from emails.template import JinjaTemplate
from emails.compat import NativeStringIO, to_bytes

View File

@ -1,12 +1,9 @@
# coding: utf-8
from __future__ import unicode_literals
from __future__ import unicode_literals, print_function
import emails
from emails.compat import StringIO
from emails.template import JinjaTemplate
from emails.compat import NativeStringIO, to_bytes
from .helpers import TRAVIS_CI, HAS_INTERNET_CONNECTION, _email_data, common_email_data
from emails.compat import to_unicode
from .helpers import common_email_data
def test_message_build():
@ -18,7 +15,6 @@ def test_message_build():
def test_property_works():
m = emails.Message(subject='A')
assert m._subject == 'A'
m.subject = 'C'
assert m._subject == 'C'
@ -34,7 +30,9 @@ def test_after_build():
m = emails.Message(**kwargs)
m.after_build = my_after_build
assert AFTER_BUILD_HEADER in m.as_string()
s = m.as_string()
print("type of message.as_string() is {0}".format(type(s)))
assert AFTER_BUILD_HEADER in to_unicode(s, 'utf-8')
# TODO: more tests here

View File

@ -1,79 +1,40 @@
# coding: utf-8
from __future__ import unicode_literals
import logging
import os
from emails.loader import cssinliner
import emails
from emails.compat import StringIO
from emails.template import JinjaTemplate
from emails.compat import NativeStringIO, to_bytes
import emails.loader
from .helpers import TRAVIS_CI, HAS_INTERNET_CONNECTION, _email_data, common_email_data
from .helpers import HAS_INTERNET_CONNECTION, common_email_data
try:
from local_settings import SMTP_SERVER, SMTP_PORT, SMTP_SSL, SMTP_USER, SMTP_PASSWORD
SMTP_DATA = {'host': SMTP_SERVER, 'port': SMTP_PORT,
'ssl': SMTP_SSL, 'user': SMTP_USER, 'password': SMTP_PASSWORD,
'debug': 0}
except ImportError:
SMTP_DATA = None
def test_send1():
URL = 'http://icdn.lenta.ru/images/2013/08/07/14/20130807143836932/top7_597745dde10ef36605a1239b0771ff62.jpg'
data = _email_data()
data['attachments'] = [emails.store.LazyHTTPFile(uri=URL), ]
m = emails.html(**data)
m.render(name='Полина')
assert m.subject == 'Hello, Полина'
if HAS_INTERNET_CONNECTION:
r = m.send(smtp=SMTP_DATA)
def test_send3():
data = _email_data(subject='[test python-emails] email with attachments')
def test_send_attachment(smtp_servers):
"""
Test email with attachment
"""
URL = 'http://lavr.github.io/python-emails/tests/campaignmonitor-samples/sample-template/images/gallery.png'
data = common_email_data(subject='Single attachment', attachments=[emails.store.LazyHTTPFile(uri=URL), ])
m = emails.html(**data)
if HAS_INTERNET_CONNECTION:
r = m.send(render={'name': u'Полина'}, smtp=SMTP_DATA)
for d in smtp_servers:
d.patch_message(m)
r = m.send(smtp=d.params)
def test_send2():
data = _email_data()
loader = emails.loader.HTTPLoader(filestore=emails.store.MemoryFileStore())
URL = 'http://lavr.github.io/python-emails/tests/campaignmonitor-samples/sample-template/template-widgets.html'
loader.load_url(URL, css_inline=True, make_links_absolute=True, update_stylesheet=True)
data['html'] = loader.html
data['attachments'] = loader.attachments_dict
loader.save_to_file('test_send2.html')
def test_send_with_render(smtp_servers):
data = common_email_data(subject='Render with name=John')
m = emails.html(**data)
m.render(name='Полина')
if HAS_INTERNET_CONNECTION:
r = m.send(smtp=SMTP_DATA)
r = m.send(to='s.lavrinenko@gmail.com', smtp=SMTP_DATA)
for d in smtp_servers:
d.patch_message(m)
r = m.send(render={'name': u'John'}, smtp=d.params)
def test_send_inline_images():
data = _email_data()
loader = emails.loader.HTTPLoader(filestore=emails.store.MemoryFileStore())
URL = 'http://lavr.github.io/python-emails/tests/campaignmonitor-samples/sample-template/template-widgets.html'
loader.load_url(URL, css_inline=True, make_links_absolute=True, update_stylesheet=True)
for img in loader.iter_image_links():
link = img.link
file = loader.filestore.by_uri(link, img.link_history)
img.link = "cid:%s" % file.filename
for file in loader.filestore:
file.content_disposition = 'inline'
data['html'] = loader.html
data['attachments'] = loader.attachments_dict
# loader.save_to_file('test_send_inline_images.html')
m = emails.html(**data)
m.render(name='Полина')
def test_send_with_inline_images(smtp_servers):
url = 'http://lavr.github.io/python-emails/tests/campaignmonitor-samples/sample-template/template-widgets.html'
data = common_email_data(subject='Sample html with inline images')
del data['html']
m = emails.loader.from_url(url=url, message_params=data, images_inline=True)
if HAS_INTERNET_CONNECTION:
r = m.send(smtp=SMTP_DATA)
if r.status_code != 250:
logging.error("Error sending email, response=%s" % r)
for d in smtp_servers:
d.patch_message(m)
r = m.send(smtp=d.params)

View File

@ -1,13 +1,13 @@
# encoding: utf-8
from __future__ import unicode_literals
import emails
import emails.store
def test_lazy_http():
IMG_URL = 'http://lavr.github.io/python-emails/tests/python-logo.gif'
f = emails.store.LazyHTTPFile(uri=IMG_URL)
assert f.filename == 'python-logo.gif'
assert f.content_disposition is None
assert f.content_disposition == 'attachment'
assert len(f.data) == 2549
@ -20,3 +20,9 @@ def test_store_commons():
for (k, v) in orig_file.items():
assert v == getattr(stored_file, k)
def test_store_unique_name():
store = emails.store.MemoryFileStore()
f1 = store.add({'uri': '/a/c.gif'})
assert f1.filename == 'c.gif'
f2 = store.add({'uri': '/a/b/c.gif'})
assert f2.filename == 'c-2.gif'

View File

@ -0,0 +1,14 @@
# encoding: utf-8
import emails, emails.loader
def test_loader_example():
base_url = 'http://lavr.github.io/python-emails/tests/campaignmonitor-samples/sample-template/'
URL = base_url + 'template-widgets.html'
message = emails.Message.from_loader(loader=emails.loader.from_url(URL),
mail_from=('ABC', 'robot@mycompany.com'),
subject="Newsletter")
print(message.as_string())

View File

@ -0,0 +1,11 @@
# encoding: utf-8
from __future__ import unicode_literals
from emails.utils import parse_name_and_email
def test_parse_name_and_email():
assert parse_name_and_email('john@smith.me') == (None, 'john@smith.me')
assert parse_name_and_email('"John Smith" <john@smith.me>') == \
('John Smith', 'john@smith.me')
assert parse_name_and_email(['John Smith', 'john@smith.me']) == \
('John Smith', 'john@smith.me')

View File

@ -0,0 +1,42 @@
# encoding: utf-8
from __future__ import unicode_literals
from emails.transformer import Transformer
def test_image_apply():
pairs = [
("""<div style="background: url(3.png);"></div>""",
"""<div style="background: url(A/3.png)"></div>"""),
("""<img src="4.png">""",
"""<img src="A/4.png">"""),
("""<table background="5.png">""",
"""<table background="A/5.png">""")
]
def func(uri, **kw):
return "A/"+uri
for before, after in pairs:
t = Transformer(html=before)
t.apply_to_images(func)
assert after in t.to_string()
def test_link_apply():
pairs = [
("""<a href="1"></a>""",
"""<a href="A/1"></a>"""),
]
def func(uri, **kw):
return "A/"+uri
for before, after in pairs:
t = Transformer(html=before)
t.apply_to_links(func)
assert after in t.to_string()

315
emails/transformer.py Normal file
View File

@ -0,0 +1,315 @@
# encoding: utf-8
from __future__ import unicode_literals
import posixpath
import os.path
import logging
import re
import warnings
from cssutils import CSSParser
from lxml import etree
from premailer import Premailer
from premailer.premailer import ExternalNotFoundError
import emails
from emails.compat import urlparse, to_unicode, to_bytes, text_type
from emails.store import MemoryFileStore, LazyHTTPFile
from .loader.local_store import FileNotFound
class LocalPremailer(Premailer):
def __init__(self, html, local_loader=None, **kw):
if 'preserve_internal_links' not in kw:
kw['preserve_internal_links'] = True
self.local_loader = local_loader
super(LocalPremailer, self).__init__(html=html, **kw)
def _load_external(self, url):
"""
loads an external stylesheet from a remote url or local store
"""
if url.startswith('//'):
# then we have to rely on the base_url
if self.base_url and 'https://' in self.base_url:
url = 'https:' + url
else:
url = 'http:' + url
if url.startswith('http://') or url.startswith('https://'):
content = self._load_external_url(url)
else:
content = None
if self.local_loader:
try:
content = self.local_loader.get_source(url)
except FileNotFound:
content = None
if content is None:
if self.base_url:
return self._load_external(urlparse.urljoin(self.base_url, url))
else:
raise ExternalNotFoundError(url)
return content
class HTMLParser(object):
_cdata_regex = re.compile(r'\<\!\[CDATA\[(.*?)\]\]\>', re.DOTALL)
def __init__(self, html, method="html"):
self._html = html
self._method = method
self._tree = None
@property
def html(self):
return self._html
@property
def tree(self):
if self._tree is None:
parser = self._method == 'xml' \
and etree.XMLParser(ns_clean=False, resolve_entities=False) \
or etree.HTMLParser()
self._tree = etree.fromstring(self._html.strip(), parser)
return self._tree
def to_string(self, encoding='utf-8', **kwargs):
out = etree.tostring(self.tree, encoding=encoding, method=self._method, **kwargs).decode(encoding)
if self._method == 'xml':
out = self._cdata_regex.sub(
lambda m: '/*<![CDATA[*/%s/*]]>*/' % m.group(1),
out
)
return out
def apply_to_images(self, func, images=True, backgrounds=True, styles_uri=True):
def _apply_to_style_uri(style_text, func):
dirty = False
parser = CSSParser().parseStyle(style_text)
for prop in parser.getProperties(all=True):
for value in prop.propertyValue:
if value.type == 'URI':
old_uri = value.uri
new_uri = func(old_uri, element=value)
if new_uri != old_uri:
dirty = True
value.uri = new_uri
if dirty:
return to_unicode(parser.cssText, 'utf-8')
else:
return style_text
if images:
# Apply to images from IMG tag
for img in self.tree.xpath(".//img"):
if 'src' in img.attrib:
img.attrib['src'] = func(img.attrib['src'], element=img)
if backgrounds:
# Apply to images from <tag background="X">
for item in self.tree.xpath("//@background"):
tag = item.getparent()
tag.attrib['background'] = func(tag.attrib['background'], element=tag)
if styles_uri:
# Apply to style uri
for item in self.tree.xpath("//@style"):
tag = item.getparent()
tag.attrib['style'] = _apply_to_style_uri(tag.attrib['style'], func=func)
def apply_to_links(self, func):
# Apply to images from IMG tag
for a in self.tree.xpath(".//a"):
if 'href' in a.attrib:
a.attrib['href'] = func(a.attrib['href'], element=a)
def add_content_type_meta(self, content_type="text/html", charset="utf-8", element_cls=etree.Element):
def _get_content_type_meta(head):
content_type_meta = None
for meta in head.find('meta') or []:
http_equiv = meta.get('http-equiv', None)
if http_equiv and (http_equiv.lower() == 'content_type'):
content_type_meta = meta
break
if content_type_meta is None:
content_type_meta = element_cls('meta')
head.append(content_type_meta)
return content_type_meta
head = self.tree.find('head')
if head is None:
logging.warning('HEAD not found. This should not happen. Skip.')
return
meta = _get_content_type_meta(head)
meta.set('content', '%s; charset=%s' % (content_type, charset))
meta.set('http-equiv', "Content-Type")
class BaseTransformer(HTMLParser):
UNSAFE_TAGS = ['script', 'object', 'iframe', 'frame', 'base', 'meta', 'link', 'style']
attachment_store_cls = MemoryFileStore
attachment_file_cls = LazyHTTPFile
def __init__(self, html, local_loader=None,
attachment_store=None,
requests_params=None, method="html", base_url=None):
HTMLParser.__init__(self, html=html, method=method)
self.attachment_store = attachment_store if attachment_store is not None else self.attachment_store_cls()
self.local_loader = local_loader
self.base_url = base_url
self.requests_params = requests_params
def get_absolute_url(self, url):
if not self.base_url:
return url
if url.startswith('//'):
if 'https://' in self.base_url:
url = 'https:' + url
else:
url = 'http:' + url
return url
if not (url.startswith('http://') or url.startswith('https://')):
url = urlparse.urljoin(self.base_url, posixpath.normpath(url))
return url
def _load_attachment_func(self, uri, element=None, **kw):
#
# Load uri from remote url or from local_store
# Return local uri
#
attachment = self.attachment_store.by_uri(uri)
if attachment is None:
attachment = self.attachment_file_cls(
uri=uri,
absolute_url=self.get_absolute_url(uri),
local_loader=self.local_loader,
requests_args=self.requests_params)
self.attachment_store.add(attachment)
return attachment.filename
def remove_unsafe_tags(self):
for tag in self.UNSAFE_TAGS:
for el in self.tree.xpath(".//%s" % tag):
parent = el.getparent()
if parent is not None:
parent.remove(el)
def load_and_transform(self,
css_inline=True,
remove_unsafe_tags=True,
make_links_absolute=True,
set_content_type_meta=True,
update_stylesheet=True,
load_images=True,
images_inline=False,
**kw):
if not make_links_absolute:
# Now we use Premailer that always makes links absolute
warnings.warn("make_links_absolute=False is deprecated.", DeprecationWarning)
if not css_inline:
# Premailer always makes inline css.
warnings.warn("css_inline=False is deprecated.", DeprecationWarning)
if update_stylesheet:
# Premailer has no such feature.
warnings.warn("update_stylesheet=True is deprecated.", DeprecationWarning)
# 1. Premailer make some transformations on self.root tree:
# - load external css and make css inline
# - make absolute href and src if base_url is set
premailer = LocalPremailer(html=self.tree,
local_loader=self.local_loader,
method=self._method,
base_url=self.base_url,
**kw)
premailer.transform()
# 2. Load linked images and transform links
if load_images:
self.apply_to_images(self._load_attachment_func)
# 3. Remove unsafe tags is requested
if remove_unsafe_tags:
self.remove_unsafe_tags()
# 4. Set <meta> content-type
if set_content_type_meta:
# TODO: may be remove this ?
self.add_content_type_meta()
# 5. Make images inline
if load_images and images_inline:
for a in self.attachment_store:
a.is_inline = True
self.synchronize_inline_images()
def synchronize_inline_images(self, inline_names=None, non_inline_names=None):
"""
Set img src in html for images, marked as "inline" in attachments_store
"""
if inline_names is None or non_inline_names is None:
inline_names = {}
non_inline_names = {}
for a in self.attachment_store:
if a.is_inline:
inline_names[a.filename] = a.content_id
else:
non_inline_names[a.content_id] = a.filename
def _src_update_func(src, **kw):
if src.startswith('cid:'):
content_id = src[4:]
if content_id in non_inline_names:
return non_inline_names[content_id]
else:
if src in inline_names:
return 'cid:'+inline_names[src]
return src
self.apply_to_images(_src_update_func)
class Transformer(BaseTransformer):
@staticmethod
def from_message(cls, message, **kw):
return cls(html=message.html, attachment_store=message.attachments, **kw)
def to_message(self, message=None):
if message is None:
message = emails.Message()
message.html_body = self.to_string()
# TODO: Copy attachments may be.
message._attachments = self.attachment_store
class MessageTransformer(BaseTransformer):
def __init__(self, message, **kw):
self.message = message
params = {'html': message._html, 'attachment_store': message.attachments}
params.update(kw)
BaseTransformer.__init__(self, **params)
def save(self):
self.message._html = self.to_string()

View File

@ -1,5 +1,8 @@
# encoding: utf-8
from __future__ import unicode_literals
import emails
import requests
from emails.exc import HTTPLoaderError
__all__ = ['parse_name_and_email', 'load_email_charsets', 'MessageID']
@ -190,9 +193,27 @@ class SafeMIMEMultipart(MIMEMixin, MIMEMultipart):
def __setitem__(self, name, val):
MIMEMultipart.__setitem__(self, name, val)
def test_parse_name_and_email():
assert parse_name_and_email('john@smith.me') == ('', 'john@smith.me')
assert parse_name_and_email('"John Smith" <john@smith.me>') == \
('John Smith', 'john@smith.me')
assert parse_name_and_email(['John Smith', 'john@smith.me']) == \
('John Smith', 'john@smith.me')
DEFAULT_REQUESTS_PARAMS = dict(allow_redirects=True,
verify=False, timeout=10,
headers={'User-Agent': emails.USER_AGENT})
def fetch_url(url, valid_http_codes=(200, ), requests_args=None):
args = {}
args.update(DEFAULT_REQUESTS_PARAMS)
args.update(requests_args or {})
r = requests.get(url, **args)
if valid_http_codes and (r.status_code not in valid_http_codes):
raise HTTPLoaderError('Error loading url: %s. HTTP status: %s' % (url, r.status_code))
return r
def encode_header(value, charset='utf-8'):
value = to_unicode(value, charset=charset)
if isinstance(value, string_types):
value = value.rstrip()
_r = Header(value, charset)
return str(_r)
else:
return value

View File

@ -3,3 +3,4 @@ lxml
chardet
python-dateutil
requests
premailer

View File

@ -1,6 +1,6 @@
--requirement=base.txt
--requirement=tests-base.txt
jinja2
mako
django==1.6
lamson
ordereddict

View File

@ -1,6 +1,5 @@
--requirement=base.txt
--requirement=tests-base.txt
jinja2
mako
django
lamson
lamson

View File

@ -1,5 +1,4 @@
--requirement=base.txt
--requirement=tests-base.txt
jinja2
mako
django
django

View File

@ -1,5 +1,4 @@
--requirement=base.txt
--requirement=tests-base.txt
jinja2
mako
django
django

View File

@ -0,0 +1,3 @@
jinja2
mako
pytest

View File

@ -7,13 +7,15 @@ Simple utility that imports html from url ang print generated rfc822 message to
Example usage:
$ python make_rfc822.py --url=http://lavr.github.io/python-emails/tests/campaignmonitor-samples/sample-template/template-widgets.html
--subject="Some subject"
--from-name="Sergey Lavrinenko"
--from-email=s@lavr.me
--message-id-domain=localhost
--send-test-email-to=sergei-nko@mail.ru
--smtp-host=mxs.mail.ru
$ python make_rfc822.py --url=http://lavr.github.io/python-emails/tests/campaignmonitor-samples/sample-template/template-widgets.html \
--subject="Some subject" \
--from-name="Sergey Lavrinenko" \
--from-email=s@lavr.me \
--message-id-domain=localhost \
--add-header="X-Test-Header: Test" \
--add-header-imported-from \
--send-test-email-to=sergei-nko@mail.ru \
--smtp-host=mxs.mail.ru \
--smtp-port=25
Copyright 2013 Sergey Lavrinenko <s@lavr.me>
@ -32,7 +34,6 @@ from emails.template import JinjaTemplate as T
class MakeRFC822:
def __init__(self, options):
self.options = options
@ -41,9 +42,14 @@ class MakeRFC822:
--add-header "X-Source: AAA"
"""
r = {}
for s in self.options.add_headers:
(k, v) = s.split(':', 1)
r[k] = v
if self.options.add_headers:
for s in self.options.add_headers:
(k, v) = s.split(':', 1)
r[k] = v
if self.options.add_header_imported_from:
r['X-Imported-From-URL'] = self.options.url
return r
def _get_message(self):
@ -51,23 +57,19 @@ class MakeRFC822:
options = self.options
if options.message_id_domain:
message_id = emails.utils.MessageID(domain=options.message_id_domain)
message_id = emails.MessageID(domain=options.message_id_domain)
else:
message_id = None
loader = emails.loader.from_url(url=options.url, images_inline=options.inline_images)
message = emails.Message.from_loader(loader=loader,
headers= self._headers_from_command_line(), #{'X-Imported-From-URL': options.url },
template_cls=T,
mail_from=(options.from_name, options.from_email),
subject=T(unicode(options.subject, 'utf-8')),
message_id=message_id
)
headers=self._headers_from_command_line(),
template_cls=T,
mail_from=(options.from_name, options.from_email),
subject=T(unicode(options.subject, 'utf-8')),
message_id=message_id)
return message
def _send_test_email(self, message):
options = self.options
@ -88,9 +90,10 @@ class MakeRFC822:
def _start_batch(self):
fn = self.options.batch
if not fn: return None
if not fn:
return None
if fn=='-':
if fn == '-':
f = sys.stdin
else:
f = open(fn, 'rb')
@ -98,16 +101,16 @@ class MakeRFC822:
def wrapper():
for l in f.readlines():
l = l.strip()
if not l: continue
# Magic is here
if not l:
continue
try:
# Try to parse line as json
yield json.loads(l)
except ValueError:
# If it is not json, we expect one word with '@' sign
assert len(l.split())==1
assert len(l.split()) == 1
print l
login, domain = l.split('@') # ensure there is something email-like
login, domain = l.split('@') # ensure there is something email-like
yield {'to': l}
return wrapper()
@ -115,7 +118,7 @@ class MakeRFC822:
def _generate_batch(self, batch, message):
n = 0
for values in batch:
message.set_mail_to( values['to'] )
message.set_mail_to(values['to'])
message.render(**values.get('data', {}))
s = message.as_string()
n += 1
@ -124,33 +127,23 @@ class MakeRFC822:
def main(self):
options = self.options
message = self._get_message()
self._send_test_email(message)
if self.options.batch:
batch = self._start_batch()
self._generate_batch(batch, message)
else:
batch = None
if self.options.output_format=='eml':
if self.options.output_format == 'eml':
print(message.as_string())
elif self.options.output_format=='html':
elif self.options.output_format == 'html':
print(message.html_body)
self._send_test_email(message)
if __name__=="__main__":
parser = argparse.ArgumentParser(description='Simple utility that imports html from url ang print generated rfc822 message to console.')
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description='Imports html from url ang generate rfc822 message.')
parser.add_argument("-u", "--url", metavar="URL", dest="url", action="store", default=None, required=True)
@ -160,6 +153,8 @@ if __name__=="__main__":
parser.add_argument("--message-id-domain", dest="message_id_domain", default=None, required=True)
parser.add_argument("--add-header", dest="add_headers", action='append', default=None, required=False)
parser.add_argument("--add-header-imported-from", dest="add_header_imported_from", default=False,
action="store_true")
parser.add_argument("--inline-images", action="store_true", dest="inline_images", default=False)
@ -174,12 +169,12 @@ if __name__=="__main__":
parser.add_argument("--smtp-password", dest="smtp_password", default=None)
parser.add_argument("--smtp-debug", dest="smtp_debug", action="store_true")
parser.add_argument("--batch", dest="batch", default=None)
parser.add_argument("--batch", dest="batch", default=None)
parser.add_argument("--batch-start", dest="batch_start", default=None)
parser.add_argument("--batch-limit", dest="batch_limit", default=None)
options = parser.parse_args()
logging.basicConfig( level=logging.getLevelName(options.log_level.upper()) )
logging.basicConfig(level=logging.getLevelName(options.log_level.upper()))
MakeRFC822(options=options).main()

View File

@ -56,26 +56,28 @@ class run_audit(Command):
else:
print("No problems found in sourcecode.")
import emails
settings.update(
name='emails',
version='0.1.13',
version=emails.__version__,
description='Elegant and simple email library for python 2/3',
long_description=open('README.rst').read(),
author='Sergey Lavrinenko',
author_email='s@lavr.me',
url='https://github.com/lavr/python-emails',
packages = ['emails',
'emails.compat',
'emails.loader',
'emails.store',
'emails.smtp',
'emails.template',
'emails.packages',
'emails.packages.cssselect',
'emails.packages.dkim'
],
scripts=[ 'scripts/make_rfc822.py' ],
install_requires = [ 'cssutils', 'lxml', 'chardet', 'python-dateutil', 'requests' ],
packages=['emails',
'emails.compat',
'emails.loader',
'emails.store',
'emails.smtp',
'emails.template',
'emails.packages',
'emails.packages.cssselect',
'emails.packages.dkim'
],
scripts=['scripts/make_rfc822.py'],
install_requires=['cssutils', 'lxml', 'chardet', 'python-dateutil', 'requests', 'premailer'],
license=open('LICENSE').read(),
#test_suite = "emails.testsuite.test_all",
zip_safe=False,

26
tox.ini Normal file
View File

@ -0,0 +1,26 @@
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist = py26, py27, py33, py34
[testenv]
commands = py.test
[testenv:py26]
deps =
-rrequirements/tests-2.6.txt
[testenv:py27]
deps =
-rrequirements/tests-2.7.txt
[testenv:py33]
deps =
-rrequirements/tests-3.3.txt
[testenv:py34]
deps =
-rrequirements/tests-3.4.txt