') or None
+ return self._mail_from
+
+ mail_from = property(get_mail_from, set_mail_from)
+
def set_mail_to(self, mail_to):
# Now we parse only one to-addr
# TODO: parse list of to-addrs
@@ -121,25 +116,40 @@ class Message(object):
self._html = html
self._html_url = url
+ def get_html(self):
+ return self._html
+
+ html = property(get_html, set_html)
+
def set_text(self, text, url=None):
if hasattr(text, 'read'):
text = text.read()
self._text = text
self._text_url = url
- def attach(self, **kwargs):
- if 'content_disposition' not in kwargs:
- kwargs['content_disposition'] = 'attachment'
- self.attachments.add(kwargs)
+ def get_text(self):
+ return self._text
+
+ text = property(get_text, set_text)
@classmethod
def from_loader(cls, loader, template_cls=None, **kwargs):
"""
- Get html and attachments from HTTPLoader
+ Get html and attachments from Loader
"""
- message = cls(html=template_cls and template_cls(loader.html) or loader.html, **kwargs)
- for att in loader.filestore:
- message.attach(**att.as_dict())
+
+ html = loader.html
+ if html and template_cls:
+ html = template_cls(html)
+
+ text = loader.text
+ if text and template_cls:
+ text = template_cls(text)
+
+ message = cls(html=html, text=text, **kwargs)
+
+ for attachment in loader.attachments:
+ message.attach(**attachment.as_dict())
return message
@property
@@ -164,12 +174,6 @@ class Message(object):
def render(self, **kwargs):
self.render_data = kwargs
- @property
- def attachments(self):
- if self._attachments is None:
- self._attachments = self.filestore_cls(self.attachment_cls)
- return self._attachments
-
def set_date(self, value):
if isinstance(value, string_types):
_d = dateutil_parse(value)
@@ -197,13 +201,7 @@ class Message(object):
return is_callable(mid) and mid() or mid
def encode_header(self, value):
- value = to_unicode(value, charset=self.charset)
- if isinstance(value, string_types):
- value = value.rstrip()
- _r = Header(value, self.charset)
- return str(_r)
- else:
- return value
+ return encode_header_(value, self.charset)
def encode_name_header(self, realname, email):
if realname:
@@ -222,18 +220,29 @@ class Message(object):
if '\n' in value or '\r' in value:
raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (value, key))
- if key.lower() in ADDRESS_HEADERS:
+ if key.lower() in self.ADDRESS_HEADERS:
value = ', '.join(sanitize_address(addr, self.charset)
for addr in getaddresses((value,)))
msg[key] = encode and self.encode_header(value) or value
+ @property
+ def attachments(self):
+ if self._attachments is None:
+ self._attachments = self.filestore_cls(self.attachment_cls)
+ return self._attachments
+
+ def attach(self, **kwargs):
+ if 'content_disposition' not in kwargs:
+ kwargs['content_disposition'] = 'attachment'
+ self.attachments.add(kwargs)
+
def _build_message(self, message_cls=None):
message_cls = message_cls or SafeMIMEMultipart
msg = message_cls()
- msg.preamble = ROOT_PREAMBLE
+ msg.preamble = self.ROOT_PREAMBLE
self.set_header(msg, 'Date', self.message_date, encode=False)
self.set_header(msg, 'Message-ID', self.message_id(), encode=False)
@@ -255,8 +264,11 @@ class Message(object):
mail_to = self._mail_to and self.encode_name_header(*self._mail_to[0]) or None
self.set_header(msg, 'To', mail_to, encode=False)
+ msgrel = SafeMIMEMultipart('related')
+ msg.attach(msgrel)
+
msgalt = SafeMIMEMultipart('alternative')
- msg.attach(msgalt)
+ msgrel.attach(msgalt)
_text = self.text_body
_html = self.html_body
@@ -275,34 +287,23 @@ class Message(object):
msgalt.attach(msghtml)
for f in self.attachments:
- msgfile = f.mime
- if msgfile:
- msg.attach(msgfile)
+ part = f.mime
+ if part:
+ if f.is_inline:
+ msgrel.attach(part)
+ else:
+ msg.attach(part)
if self.after_build:
self.after_build(self, msg)
return msg
- def message(self, message_cls=None):
- msg = self._build_message(message_cls=message_cls)
- if self._dkim_signer:
- msg_str = msg.as_string()
- dkim_header = self._dkim_signer.get_sign_header(to_bytes(msg_str))
- if dkim_header:
- msg._headers.insert(0, dkim_header)
- return msg
- def as_string(self):
- # self.as_string() is not equialent self.message().as_string()
- # self.as_string() gets one less message-to-string conversions for dkim
- msg = self._build_message()
- r = msg.as_string()
- if self._dkim_signer:
- dkim_header = self._dkim_signer.get_sign(to_bytes(r))
- if dkim_header:
- r = dkim_header + r
- return r
+class MessageSendMixin(object):
+
+ smtp_pool_factory = ObjectFactory
+ smtp_cls = SMTPBackend
@property
def smtp_pool(self):
@@ -311,9 +312,6 @@ class Message(object):
pool = self._smtp_pool = self.smtp_pool_factory(cls=self.smtp_cls)
return pool
- def dkim(self, **kwargs):
- self._dkim_signer = self.dkim_cls(**kwargs)
-
def send(self,
to=None,
set_mail_to=True,
@@ -361,7 +359,7 @@ class Message(object):
from_addr = self._mail_from[1]
if not from_addr:
- raise ValueError('No from-addr')
+ raise ValueError('No "from" addr')
params = dict(from_addr=from_addr,
to_addrs=[to_addr, ],
@@ -376,6 +374,105 @@ class Message(object):
return response[0]
+class MessageTransformerMixin(object):
+
+ transformer_cls = None
+
+ def create_transformer(self, **kw):
+ cls = self.transformer_cls
+ if cls is None:
+ from emails.transformer import MessageTransformer
+ cls = MessageTransformer
+
+ self._transformer = cls(message=self, **kw)
+ return self._transformer
+
+ def destroy_transformer(self):
+ self._transformer = None
+
+ @property
+ def transformer(self):
+ t = getattr(self, '_transformer', None)
+ if t is None:
+ t = self.create_transformer()
+ return t
+
+
+class Message(BaseMessage, MessageSendMixin, MessageTransformerMixin):
+ """
+ Email message with:
+ - DKIM signer
+ - smtp send
+ - Message.transformer object
+ """
+
+ dkim_cls = DKIMSigner
+
+ def __init__(self, **kwargs):
+ BaseMessage.__init__(self, **kwargs)
+ self._dkim_signer = None
+ self.after_build = None
+
+ def dkim(self, **kwargs):
+ self._dkim_signer = self.dkim_cls(**kwargs)
+
+ def set_html(self, **kw):
+ # When html set, remove old transformer
+ self.destroy_transformer()
+ super(Message, self).set_html(**kw)
+
+ def as_message(self, message_cls=None):
+ msg = self._build_message(message_cls=message_cls)
+ if self._dkim_signer:
+ msg_str = msg.as_string()
+ dkim_header = self._dkim_signer.get_sign_header(to_bytes(msg_str))
+ if dkim_header:
+ msg._headers.insert(0, dkim_header)
+ return msg
+
+ message = as_message
+
+ def as_string(self):
+ # self.as_string() is not equialent self.message().as_string()
+ # self.as_string() gets one less message-to-string conversions for dkim
+ msg = self._build_message()
+ r = msg.as_string()
+ if self._dkim_signer:
+ dkim_header = self._dkim_signer.get_sign(to_bytes(r))
+ if dkim_header:
+ r = dkim_header + r
+ return r
+
+
def html(**kwargs):
return Message(**kwargs)
+
+class DjangoMessageProxy(object):
+
+ """
+ Class looks like django.core.mail.EmailMessage for standard django email backend.
+
+ Example usage:
+
+ message = emails.Message(html='...', subject='...', mail_from='robot@company.ltd')
+ connection = django.core.mail.get_connection()
+
+ message.set_mail_to('somebody@somewhere.net')
+ connection.send_messages([DjangoMessageProxy(message), ])
+ """
+
+ def __init__(self, message, recipients=None, context=None):
+ self._message = message
+ self._recipients = recipients
+ self._context = context and context.copy() or {}
+
+ self.from_email = message.mail_from[1]
+ self.encoding = message.charset
+
+ def recipients(self):
+ return self._recipients or [r[1] for r in self._message.mail_to]
+
+ def message(self):
+ self._message.render(**self._context)
+ return self._message.message()
diff --git a/emails/smtp/backend.py b/emails/smtp/backend.py
index 31098ab..c8581f3 100644
--- a/emails/smtp/backend.py
+++ b/emails/smtp/backend.py
@@ -1,11 +1,10 @@
# encoding: utf-8
from __future__ import unicode_literals
-__all__ = [ 'SMTPSender' ]
+__all__ = ['SMTPBackend']
import smtplib
import logging
-import threading
from functools import wraps
from .client import SMTPResponse, SMTPClientWithResponse, SMTPClientWithResponse_SSL
@@ -31,25 +30,19 @@ class SMTPBackend:
def __init__(self,
- user=None,
- password=None,
ssl=False,
- tls=False,
- debug=False,
fail_silently=True,
**kwargs):
self.smtp_cls = ssl and self.connection_ssl_cls or self.connection_cls
- self.debug = debug
+
self.ssl = ssl
- self.tls = tls
+ self.tls = kwargs.get('tls')
if self.ssl and self.tls:
raise ValueError(
"ssl/tls are mutually exclusive, so only set "
"one of those settings to True.")
- self.user = user
- self.password = password
if 'timeout' not in kwargs:
kwargs['timeout'] = self.DEFAULT_SOCKET_TIMEOUT
self.smtp_cls_kwargs = kwargs
@@ -59,10 +52,8 @@ class SMTPBackend:
self.fail_silently = fail_silently
self.connection = None
#self.local_hostname=DNS_NAME.get_fqdn()
- self._lock = threading.RLock()
def open(self):
- #logger.debug('SMTPSender _connect')
if self.connection is None:
self.connection = self.smtp_cls(parent=self, **self.smtp_cls_kwargs)
self.connection.initialize()
@@ -83,7 +74,6 @@ class SMTPBackend:
finally:
self.connection = None
-
def make_response(self, exception=None):
return self.response_cls(host=self.host, port=self.port, exception=exception)
@@ -105,21 +95,17 @@ class SMTPBackend:
def sendmail(self, from_addr, to_addrs, msg, mail_options=[], rcpt_options=[]):
- if not to_addrs: return False
+ if not to_addrs:
+ return False
if not isinstance(to_addrs, (list, tuple)):
to_addrs = [to_addrs, ]
- #from_addr = sanitize_address(from_addr, email_message.encoding)
- #to_addrs = [sanitize_address(addr, email_message.encoding) for addr in to_addrs]
- #message = email_message.message()
- #charset = message.get_charset().get_output_charset() if message.get_charset() else 'utf-8'
-
try:
self.open()
except (IOError, smtplib.SMTPException) as e:
logger.exception("Error connecting smtp server")
- response = self.make_response(exception = e)
+ response = self.make_response(exception=e)
if not self.fail_silently:
response.raise_if_needed()
return [response, ]
@@ -133,7 +119,6 @@ class SMTPBackend:
rcpt_options=rcpt_options)
if not self.fail_silently:
- [ r.raise_if_needed() for r in response ]
+ [r.raise_if_needed() for r in response]
return response
-
diff --git a/emails/smtp/client.py b/emails/smtp/client.py
index ab492a4..47fde2e 100644
--- a/emails/smtp/client.py
+++ b/emails/smtp/client.py
@@ -1,10 +1,8 @@
# encoding: utf-8
-
-__all__ = [ 'SMTPResponse', 'SMTPClientWithResponse', 'SMTPClientWithResponse_SSL' ]
+__all__ = ['SMTPResponse', 'SMTPClientWithResponse', 'SMTPClientWithResponse_SSL']
from smtplib import _have_ssl, SMTP
import smtplib
-
import logging
logger = logging.getLogger(__name__)
@@ -17,7 +15,6 @@ class SMTPResponse(object):
self.ssl = ssl
self.responses = []
self.exception = exception
- #self.complete = False
self.success = None
self.from_addr = None
self.esmtp_opts = None
@@ -28,7 +25,7 @@ class SMTPResponse(object):
self.last_command = None
def set_status(self, command, code, text):
- self.responses.append( [command, code, text] )
+ self.responses.append([command, code, text])
self.status_code = code
self.status_text = text
self.last_command = command
@@ -36,7 +33,7 @@ class SMTPResponse(object):
def set_exception(self, exc):
self.exception = exc
- def raise_if_needed():
+ def raise_if_needed(self):
if self.exception:
raise self.exception
@@ -49,9 +46,6 @@ class SMTPResponse(object):
self.status_text.__repr__())
-#class SMTPCommandsLog:
-
-
class SMTPClientWithResponse(SMTP):
def __init__(self, parent, **kwargs):
@@ -59,19 +53,17 @@ class SMTPClientWithResponse(SMTP):
self.make_response = parent.make_response
self._last_smtp_response = (None, None)
self.tls = kwargs.pop('tls', False)
- self.debug = kwargs.pop('debug', False)
+ self.ssl = kwargs.pop('ssl', False)
+ self.debug = kwargs.pop('debug', 0)
+ self.set_debuglevel(self.debug)
self.user = kwargs.pop('user', None)
self.password = kwargs.pop('password', None)
SMTP.__init__(self, **kwargs)
self.initialize()
def initialize(self):
- if self.debug:
- self.set_debuglevel(1)
if self.tls:
- self.ehlo()
self.starttls()
- self.ehlo()
if self.user:
self.login(user=self.user, password=self.password)
self.ehlo_or_helo_if_needed()
@@ -92,7 +84,6 @@ class SMTPClientWithResponse(SMTP):
self._last_smtp_response = (code, msg)
return code, msg
-
def _send_one_mail(self, from_addr, to_addr, msg, mail_options=[], rcpt_options=[]):
esmtp_opts = []
@@ -140,18 +131,17 @@ class SMTPClientWithResponse(SMTP):
return response
def sendmail(self, from_addr, to_addrs, msg, mail_options=[], rcpt_options=[]):
-
# Send one email and returns one response
if not to_addrs:
- raise StopIteration
+ return []
assert isinstance(to_addrs, (list, tuple))
if len(to_addrs)>1:
logger.warning('Beware: emails.smtp.client.SMTPClientWithResponse.sendmail sends full message to each email')
- return [ self._send_one_mail(from_addr, to_addr, msg, mail_options, rcpt_options) \
- for to_addr in to_addrs ]
+ return [self._send_one_mail(from_addr, to_addr, msg, mail_options, rcpt_options) \
+ for to_addr in to_addrs]
@@ -160,18 +150,34 @@ if _have_ssl:
from smtplib import SMTP_SSL
import ssl
- class SMTPClientWithResponse_SSL(SMTPClientWithResponse, SMTP_SSL):
+ class SMTPClientWithResponse_SSL(SMTP_SSL, SMTPClientWithResponse):
+
+ def __init__(self, **kw):
+ args = {}
+ for k in ('host', 'port', 'local_hostname', 'keyfile', 'certfile', 'timeout'):
+ if k in kw:
+ args[k] = kw[k]
+ SMTP_SSL.__init__(self, **args)
+ SMTPClientWithResponse.__init__(self, **kw)
+
+ def data(self, msg):
+ (code, msg) = SMTP.data(self, msg)
+ self._last_smtp_response = (code, msg)
+ return code, msg
def quit(self):
"""Closes the connection to the email server."""
try:
- super(self, SMTPClientWithResponse_SSL).quit()
+ SMTPClientWithResponse.quit(self)
except (ssl.SSLError, smtplib.SMTPServerDisconnected):
# This happens when calling quit() on a TLS connection
# sometimes, or when the connection was already disconnected
# by the server.
self.close()
+ def sendmail(self, *args, **kw):
+ return SMTPClientWithResponse.sendmail(self, *args, **kw)
+
else:
class SMTPClientWithResponse_SSL:
diff --git a/emails/smtp/factory.py b/emails/smtp/factory.py
index f01de0f..9e326f0 100644
--- a/emails/smtp/factory.py
+++ b/emails/smtp/factory.py
@@ -2,7 +2,7 @@
def simple_dict2str(d):
# Simple dict serializer
- return ";".join( [ "%s=%s" % (k, v) for (k, v) in d.items() ] )
+ return ";".join(["%s=%s" % (k, v) for (k, v) in d.items()])
_serializer = simple_dict2str
diff --git a/emails/store/file.py b/emails/store/file.py
index 852bd97..80a3985 100644
--- a/emails/store/file.py
+++ b/emails/store/file.py
@@ -10,8 +10,11 @@ import requests
from mimetypes import guess_type
from email.mime.base import MIMEBase
from email.encoders import encode_base64
+import emails
from emails.compat import urlparse
from emails.compat import string_types, to_bytes
+from emails.utils import fetch_url, encode_header
+
# class FileNotFound(Exception):
# pass
@@ -32,6 +35,8 @@ class BaseFile(object):
Store base "attachment-file" information.
"""
+ content_id_suffix = '@python.emails'
+
def __init__(self, **kwargs):
"""
uri and filename are connected properties.
@@ -42,12 +47,11 @@ class BaseFile(object):
self.absolute_url = kwargs.get('absolute_url', None) or self.uri
self.filename = kwargs.get('filename', None)
self.data = kwargs.get('data', None)
- self._mime_type = kwargs.get('mime_type', None)
- self._headers = kwargs.get('headers', None)
- self._content_disposition = kwargs.get('content_disposition', None)
- self.subtype = kwargs.get('subtype', None)
- self.local_loader = kwargs.get('local_loader', None)
- self.id = id
+ self._mime_type = kwargs.get('mime_type')
+ self._headers = kwargs.get('headers')
+ self._content_disposition = kwargs.get('content_disposition', 'attachment')
+ self.subtype = kwargs.get('subtype')
+ self.local_loader = kwargs.get('local_loader')
def as_dict(self, fields=None):
fields = fields or ('uri', 'absolute_url', 'filename', 'data',
@@ -119,21 +123,41 @@ class BaseFile(object):
content_disposition = property(get_content_disposition, set_content_disposition)
+ @property
+ def is_inline(self):
+ return self.content_disposition == 'inline'
+
+ @is_inline.setter
+ def is_inline(self, value):
+ if bool(value):
+ self.content_disposition = 'inline'
+ else:
+ self.content_disposition = 'attachment'
+
+ @property
+ def content_id(self):
+ return "{0}{1}".format(self.filename, self.content_id_suffix)
+
+ @staticmethod
+ def parse_content_id(cls, content_id):
+ if content_id.endswith(cls.content_id_suffix):
+ return {'filename': content_id[:-len(cls.content_id_suffix)]}
+ else:
+ return None
+
@property
def mime(self):
if self.content_disposition is None:
return None
_mime = getattr(self, '_cached_mime', None)
if _mime is None:
- filename = str(Header(self.filename, 'utf-8'))
- self._cached_mime = _mime = MIMEBase(*self.mime_type.split('/', 1))
+ filename_header = encode_header(self.filename)
+ self._cached_mime = _mime = MIMEBase(*self.mime_type.split('/', 1), name=filename_header)
_mime.set_payload(to_bytes(self.data))
encode_base64(_mime)
- _mime.add_header('Content-Disposition',
- self.content_disposition,
- filename=filename)
+ _mime.add_header('Content-Disposition', self.content_disposition, filename=filename_header)
if self.content_disposition == 'inline':
- _mime.add_header('Content-ID', '<{0}>'.format(filename))
+ _mime.add_header('Content-ID', '<%s>' % self.content_id)
return _mime
def reset_mime(self):
@@ -145,11 +169,9 @@ class BaseFile(object):
class LazyHTTPFile(BaseFile):
- def __init__(self, fetch_params=None, **kwargs):
+ def __init__(self, requests_args=None, **kwargs):
BaseFile.__init__(self, **kwargs)
- self.fetch_params = dict(allow_redirects=True, verify=False)
- if fetch_params:
- self.fetch_params.update(fetch_params)
+ self.requests_args = requests_args
self._fetched = False
def fetch(self):
@@ -162,7 +184,7 @@ class LazyHTTPFile(BaseFile):
self._data = data
return
- r = requests.get(self.absolute_url or self.uri, **self.fetch_params)
+ r = fetch_url(url=self.absolute_url or self.uri, requests_args=self.requests_args)
if r.status_code == 200:
self._data = r.content
self._headers = r.headers
diff --git a/emails/store/store.py b/emails/store/store.py
index e56ca39..1f563d8 100644
--- a/emails/store/store.py
+++ b/emails/store/store.py
@@ -18,7 +18,7 @@ class MemoryFileStore(FileStore):
if file_cls:
self.file_cls = file_cls
self._files = OrderedDict()
- self._filenames = set()
+ self._filenames = {}
def __contains__(self, k):
if isinstance(k, self.file_cls):
@@ -48,24 +48,26 @@ class MemoryFileStore(FileStore):
if v:
filename = v.filename
if filename and (filename in self._filenames):
- self._filenames.remove(filename)
+ del self._filenames[filename]
del self._files[uri]
- def unique_filename(self, filename):
+ def unique_filename(self, filename, uri=None):
- if filename not in self._filenames:
- return filename
+ if filename in self._filenames:
+ n = 1
+ basefilename, ext = splitext(filename)
- n = 1
- basefilename, ext = splitext(filename)
+ while True:
+ n += 1
+ filename = "%s-%d%s" % (basefilename, n, ext)
+ if filename not in self._filenames:
+ break
+ else:
+ self._filenames[filename] = uri
- while True:
- n += 1
- filename = "%s-%d%s" % (basefilename, n, ext)
- if filename not in self._filenames:
- return filename
+ return filename
- def add(self, value):
+ def add(self, value, replace=False):
if isinstance(value, self.file_cls):
uri = value.uri
@@ -75,24 +77,35 @@ class MemoryFileStore(FileStore):
else:
raise ValueError("Unknown file type: %s" % type(value))
- self.remove(uri)
- value.filename = self.unique_filename(value.filename)
- self._filenames.add(value.filename)
- self._files[uri] = value
+ if (uri not in self._files) or replace:
+ self.remove(uri)
+ value.filename = self.unique_filename(value.filename, uri=uri)
+ self._files[uri] = value
+ return value
- def by_uri(self, uri, synonims=None):
+ def by_uri(self, uri, synonyms=None):
r = self._files.get(uri, None)
if r:
return r
- if synonims:
- for _uri in synonims:
+ if synonyms:
+ for _uri in synonyms:
r = self._files.get(_uri, None)
if r:
return r
return None
+ def by_filename(self, filename):
+ uri = self._filenames.get(filename)
+ if uri:
+ return self.by_uri(uri)
+
+ def by_content_id(self, content_id):
+ parsed = self.file_cls.parse_content_id(content_id)
+ if parsed:
+ return self.by_filename(parsed['filename'])
+
def __getitem__(self, uri):
- return self._files.get(uri, None)
+ return self.by_uri(uri) or self.by_filename(uri)
def __iter__(self):
for k in self._files:
diff --git a/emails/testsuite/conftest.py b/emails/testsuite/conftest.py
index 581bd3b..5993e3f 100644
--- a/emails/testsuite/conftest.py
+++ b/emails/testsuite/conftest.py
@@ -7,7 +7,7 @@ import logging
import threading
import os
import os.path
-
+import datetime
import pytest
@@ -98,10 +98,82 @@ def smtp_server(request):
def django_email_backend(request):
from django.conf import settings
logger.debug('django_email_backend...')
- server = smtp_server(request)
- settings.configure(EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend',
- EMAIL_HOST=server.host, EMAIL_PORT=server.port)
+ settings.configure(EMAIL_BACKEND='django.core.mail.backends.filebased.EmailBackend',
+ EMAIL_FILE_PATH='tmp-emails')
from django.core.mail import get_connection
- SETTINGS = {}
return get_connection()
+
+class SMTPTestParams:
+
+ subject_prefix = '[test-python-emails]'
+
+ def __init__(self, from_email=None, to_email=None, defaults=None, **kw):
+ params = {}
+ params.update(defaults or {})
+ params.update(kw)
+ params['debug'] = 1
+ params['timeout'] = 15
+ self.params = params
+
+ self.from_email = from_email
+ self.to_email = to_email
+
+ def patch_message(self, message):
+ # Some SMTP requires from and to emails
+
+ if self.from_email:
+ message._mail_from = (message._mail_from[0], self.from_email)
+
+ if self.to_email:
+ message.mail_to = self.to_email
+
+ # TODO: this code breaks template in subject; deal with this
+ message.subject = " ".join([self.subject_prefix, datetime.datetime.now().strftime('%H:%M:%S'),
+ message.subject])
+
+ def __str__(self):
+ return u'SMTPTestParams(host={0}, port={1}, user={2})'.format(self.params.get('host'),
+ self.params.get('port'),
+ self.params.get('user'))
+
+
+
+@pytest.fixture(scope='module')
+def smtp_servers(request):
+
+ r = []
+
+ """
+ r.append(SMTPTestParams(from_email='drlavr@yandex.ru',
+ to_email='drlavr@yandex.ru',
+ fail_silently=False,
+ **{'host': 'mx.yandex.ru', 'port': 25, 'ssl': False}))
+
+ r.append(SMTPTestParams(from_email='drlavr+togmail@yandex.ru',
+ to_email='s.lavrinenko@gmail.com',
+ fail_silently=False,
+ **{'host': 'gmail-smtp-in.l.google.com', 'port': 25, 'ssl': False}))
+
+
+ r.append(SMTPTestParams(from_email='drlavr@yandex.ru',
+ to_email='s.lavrinenko@me.com',
+ fail_silently=False,
+ **{'host': 'mx3.mail.icloud.com', 'port': 25, 'ssl': False}))
+ """
+
+ r.append(SMTPTestParams(from_email='drlavr@yandex.ru',
+ to_email='lavr@outlook.com',
+ fail_silently=False,
+ **{'host': 'mx1.hotmail.com', 'port': 25, 'ssl': False}))
+
+ try:
+ from .local_smtp_settings import SMTP_SETTINGS_WITH_AUTH, FROM_EMAIL, TO_EMAIL
+ r.append(SMTPTestParams(from_email=FROM_EMAIL,
+ to_email=TO_EMAIL,
+ fail_silently=False,
+ **SMTP_SETTINGS_WITH_AUTH))
+ except ImportError:
+ pass
+
+ return r
\ No newline at end of file
diff --git a/emails/testsuite/smtp/test_django_integrations.py b/emails/testsuite/django_/test_django_integrations.py
similarity index 62%
rename from emails/testsuite/smtp/test_django_integrations.py
rename to emails/testsuite/django_/test_django_integrations.py
index 6f072d0..bfe1618 100644
--- a/emails/testsuite/smtp/test_django_integrations.py
+++ b/emails/testsuite/django_/test_django_integrations.py
@@ -1,6 +1,7 @@
# encoding: utf-8
from __future__ import unicode_literals
import emails
+import emails.message
def test_send_via_django_backend(django_email_backend):
@@ -9,7 +10,7 @@ def test_send_via_django_backend(django_email_backend):
Send email via django's email backend.
`django_email_backend` defined in conftest.py
"""
- message_params = {'html':'Test from python-emails',
+ message_params = {'html': '
Test from python-emails',
'mail_from': 's@lavr.me',
'mail_to': 's.lavrinenko@gmail.com',
'subject': 'Test from python-emails'}
@@ -22,3 +23,12 @@ def test_send_via_django_backend(django_email_backend):
headers = {'Reply-To': 'another@example.com'})
backend.send_messages([email, ])
+
+def test_django_message_proxy(django_email_backend):
+
+ message_params = {'html': '
Test from python-emails',
+ 'mail_from': 's@lavr.me',
+ 'mail_to': 's.lavrinenko@gmail.com',
+ 'subject': 'Test from python-emails'}
+ msg = emails.html(**message_params)
+ django_email_backend.send_messages([emails.message.DjangoMessageProxy(msg), ])
diff --git a/emails/testsuite/loader/data/html_import/oldornament.zip b/emails/testsuite/loader/data/html_import/oldornament.zip
index 1e3802d..2e18f66 100644
Binary files a/emails/testsuite/loader/data/html_import/oldornament.zip and b/emails/testsuite/loader/data/html_import/oldornament.zip differ
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/left_sidebar.html b/emails/testsuite/loader/data/html_import/oldornament/html/left_sidebar.html
deleted file mode 100755
index 4a41f96..0000000
--- a/emails/testsuite/loader/data/html_import/oldornament/html/left_sidebar.html
+++ /dev/null
@@ -1,152 +0,0 @@
-
-
-
-SET-3-old-ornament
-
-
-
-
-
-
-
-
-
-
-
- You're receiving this newsletter because you bought widgets from us.
- Having trouble reading this email? View it in your browser. Not interested anymore? Unsubscribe.
- |
-
-
-
-
-
-
-
- |
-
-
-
-
-
-
- Dear Simon,
- 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.
- Regards, ABC Widgets
- |
-
-
- |
-
-
-
-
-
-
- |
-
-
- |
-
-
-
-
-
-
-
- Lorem Ipsum Dolor Sit Amet
-
-
-
- |
-
-
- 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
-
- Fermentum Quam Etur Lectus
-
-
-
- |
-
-
- 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.
-
- Lorem Ipsum Dolor Sit Amet
-
-
-
- |
-
-
- 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
- |
-
-
-
- Forward this issue
-
- Do you know someone who might be interested in receiving this monthly newsletter?
- forward
-
- Unsubscribe
-
- You're receiving this newsletter because you signed up for the ABC Widget Newsletter.
- unsubscribe
-
- Contact us
-
- 123 Some Street
- City, State
- 99999
- (147) 789 7745
- www.abcwidgets.com
- info@abcwidgets.com
- |
-
-
-
- Back to top |
-
-
- |
-
-
- |
-
-
- |
-
-
-
-
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/right_sidebar.html b/emails/testsuite/loader/data/html_import/oldornament/html/right_sidebar.html
deleted file mode 100755
index 2e0dfa0..0000000
--- a/emails/testsuite/loader/data/html_import/oldornament/html/right_sidebar.html
+++ /dev/null
@@ -1,153 +0,0 @@
-
-
-
-SET-3-old-ornament
-
-
-
-
-
-
-
-
-
-
-
- You're receiving this newsletter because you bought widgets from us.
- Having trouble reading this email? View it in your browser. Not interested anymore? Unsubscribe.
- |
-
-
-
-
-
-
-
- |
-
-
-
-
-
-
- Dear Simon,
- 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.
- Regards, ABC Widgets
- |
-
-
- |
-
-
-
-
-
-
- |
-
-
- |
-
-
-
-
-
-
-
- Forward this issue
-
- Do you know someone who might be interested in receiving this monthly newsletter?
- forward
-
- Unsubscribe
-
- You're receiving this newsletter because you signed up for the ABC Widget Newsletter.
- unsubscribe
-
- Contact us
-
- 123 Some Street
- City, State
- 99999
- (147) 789 7745
- www.abcwidgets.com
- info@abcwidgets.com
- |
-
-
-
- Lorem Ipsum Dolor Sit Amet
-
-
-
- |
-
-
- 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
-
- Fermentum Quam Etur Lectus
-
-
-
- |
-
-
- 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.
-
- Lorem Ipsum Dolor Sit Amet
-
-
-
- |
-
-
- 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
- |
-
-
- |
-
- Back to top |
-
-
- |
-
-
- |
-
-
- |
-
-
-
-
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/arrow.png b/emails/testsuite/loader/data/html_import/oldornament/images/arrow.png
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/arrow.png
rename to emails/testsuite/loader/data/html_import/oldornament/images/arrow.png
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/banner-bottom.png b/emails/testsuite/loader/data/html_import/oldornament/images/banner-bottom.png
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/banner-bottom.png
rename to emails/testsuite/loader/data/html_import/oldornament/images/banner-bottom.png
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/banner-middle.gif b/emails/testsuite/loader/data/html_import/oldornament/images/banner-middle.gif
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/banner-middle.gif
rename to emails/testsuite/loader/data/html_import/oldornament/images/banner-middle.gif
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/banner-top.gif b/emails/testsuite/loader/data/html_import/oldornament/images/banner-top.gif
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/banner-top.gif
rename to emails/testsuite/loader/data/html_import/oldornament/images/banner-top.gif
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/bg-all.jpg b/emails/testsuite/loader/data/html_import/oldornament/images/bg-all.jpg
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/bg-all.jpg
rename to emails/testsuite/loader/data/html_import/oldornament/images/bg-all.jpg
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/bg-content.jpg b/emails/testsuite/loader/data/html_import/oldornament/images/bg-content.jpg
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/bg-content.jpg
rename to emails/testsuite/loader/data/html_import/oldornament/images/bg-content.jpg
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/bg-main.jpg b/emails/testsuite/loader/data/html_import/oldornament/images/bg-main.jpg
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/bg-main.jpg
rename to emails/testsuite/loader/data/html_import/oldornament/images/bg-main.jpg
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/divider.jpg b/emails/testsuite/loader/data/html_import/oldornament/images/divider.jpg
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/divider.jpg
rename to emails/testsuite/loader/data/html_import/oldornament/images/divider.jpg
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/divider2.jpg b/emails/testsuite/loader/data/html_import/oldornament/images/divider2.jpg
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/divider2.jpg
rename to emails/testsuite/loader/data/html_import/oldornament/images/divider2.jpg
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/flourish.png b/emails/testsuite/loader/data/html_import/oldornament/images/flourish.png
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/flourish.png
rename to emails/testsuite/loader/data/html_import/oldornament/images/flourish.png
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/img01.jpg b/emails/testsuite/loader/data/html_import/oldornament/images/img01.jpg
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/img01.jpg
rename to emails/testsuite/loader/data/html_import/oldornament/images/img01.jpg
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/img02.jpg b/emails/testsuite/loader/data/html_import/oldornament/images/img02.jpg
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/img02.jpg
rename to emails/testsuite/loader/data/html_import/oldornament/images/img02.jpg
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/img03.jpg b/emails/testsuite/loader/data/html_import/oldornament/images/img03.jpg
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/img03.jpg
rename to emails/testsuite/loader/data/html_import/oldornament/images/img03.jpg
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/images/spacer.gif b/emails/testsuite/loader/data/html_import/oldornament/images/spacer.gif
similarity index 100%
rename from emails/testsuite/loader/data/html_import/oldornament/html/images/spacer.gif
rename to emails/testsuite/loader/data/html_import/oldornament/images/spacer.gif
diff --git a/emails/testsuite/loader/data/html_import/oldornament/html/full_width.html b/emails/testsuite/loader/data/html_import/oldornament/index.html
similarity index 99%
rename from emails/testsuite/loader/data/html_import/oldornament/html/full_width.html
rename to emails/testsuite/loader/data/html_import/oldornament/index.html
index d4eadc2..7aea0d8 100755
--- a/emails/testsuite/loader/data/html_import/oldornament/html/full_width.html
+++ b/emails/testsuite/loader/data/html_import/oldornament/index.html
@@ -156,7 +156,7 @@
City, State
99999
(147) 789 7745
- www.abcwidgets.com
+ www.abcwidgets.com
info@abcwidgets.com
diff --git a/emails/testsuite/loader/test_loader.py b/emails/testsuite/loader/test_loader.py
deleted file mode 100644
index 72c92ef..0000000
--- a/emails/testsuite/loader/test_loader.py
+++ /dev/null
@@ -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 = """"""
- 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 = """ """
- 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(
- ''
- ' '
- ' ')
- 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 = """ """
- 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(""""""
- """"""
- """ """)
-
- 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 = "b"
- CSS = "a:visited {color: red;}"
- r = _do_inline_css(HTML, CSS) # , save_to_file='_result.html')
- print(r)
-
-
-def test_commons_css_inline():
- tmpl = '''style test%s'''
-
- HTML = tmpl % '''
- Style example 1
- <p>
- <p> with inline style: "color: red"
- p#x with inline style: "color: red"
- a <div> green?
- #y pink?
- '''
-
- 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("""
-
- style test
-
-
- Style example 1
- <p>
- <p> with inline style: "color: red"
- p#x with inline style: "color: red"
- a <div> green?
- #y pink?
-
-""")
-
- 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__())
-
-
diff --git a/emails/testsuite/loader/test_loaders.py b/emails/testsuite/loader/test_loaders.py
new file mode 100644
index 0000000..bf603fa
--- /dev/null
+++ b/emails/testsuite/loader/test_loaders.py
@@ -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': 'Привет!
В первых строках...',
+ '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()
\ No newline at end of file
diff --git a/emails/testsuite/message/helpers.py b/emails/testsuite/message/helpers.py
index 795b413..a0c8cb8 100644
--- a/emails/testsuite/message/helpers.py
+++ b/emails/testsuite/message/helpers.py
@@ -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': '
Привет!
В первых строках...',
- '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('
Привет, {{name}}!
В первых строках...'),
- '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('
Olá {{name}}!
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
\ No newline at end of file
+ if kw:
+ data.update(kw)
+ return data
diff --git a/emails/testsuite/message/test_dkim.py b/emails/testsuite/message/test_dkim.py
index 2e8f025..7b09597 100644
--- a/emails/testsuite/message/test_dkim.py
+++ b/emails/testsuite/message/test_dkim.py
@@ -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
diff --git a/emails/testsuite/message/test_message.py b/emails/testsuite/message/test_message.py
index d65d9d3..fd59da4 100644
--- a/emails/testsuite/message/test_message.py
+++ b/emails/testsuite/message/test_message.py
@@ -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
diff --git a/emails/testsuite/message/test_send.py b/emails/testsuite/message/test_send.py
index 1308329..c990bc9 100644
--- a/emails/testsuite/message/test_send.py
+++ b/emails/testsuite/message/test_send.py
@@ -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)
diff --git a/emails/testsuite/loader/test_store.py b/emails/testsuite/store/test_store.py
similarity index 68%
rename from emails/testsuite/loader/test_store.py
rename to emails/testsuite/store/test_store.py
index eb3aecf..32ecc90 100644
--- a/emails/testsuite/loader/test_store.py
+++ b/emails/testsuite/store/test_store.py
@@ -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'
diff --git a/emails/testsuite/test_readme.py b/emails/testsuite/test_readme.py
new file mode 100644
index 0000000..4e3f4c9
--- /dev/null
+++ b/emails/testsuite/test_readme.py
@@ -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())
diff --git a/emails/testsuite/test_utils.py b/emails/testsuite/test_utils.py
new file mode 100644
index 0000000..3ed7915
--- /dev/null
+++ b/emails/testsuite/test_utils.py
@@ -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', 'john@smith.me')
+ assert parse_name_and_email(['John Smith', 'john@smith.me']) == \
+ ('John Smith', 'john@smith.me')
\ No newline at end of file
diff --git a/emails/testsuite/transformer/test_transformer.py b/emails/testsuite/transformer/test_transformer.py
new file mode 100644
index 0000000..806e54e
--- /dev/null
+++ b/emails/testsuite/transformer/test_transformer.py
@@ -0,0 +1,42 @@
+# encoding: utf-8
+from __future__ import unicode_literals
+from emails.transformer import Transformer
+
+
+def test_image_apply():
+
+ pairs = [
+ ("""""",
+ """"""),
+
+ ("""""",
+ """"""),
+
+ ("""""",
+ """""")
+ ]
+
+ 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 = [
+ ("""""",
+ """"""),
+ ]
+
+ 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()
diff --git a/emails/transformer.py b/emails/transformer.py
new file mode 100644
index 0000000..a462409
--- /dev/null
+++ b/emails/transformer.py
@@ -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: '/**/' % 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
+ 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 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()
\ No newline at end of file
diff --git a/emails/utils.py b/emails/utils.py
index 5a6f067..77fc0bf 100644
--- a/emails/utils.py
+++ b/emails/utils.py
@@ -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', '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
\ No newline at end of file
diff --git a/requirements/base.txt b/requirements/base.txt
index e1070e1..120d407 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -3,3 +3,4 @@ lxml
chardet
python-dateutil
requests
+premailer
diff --git a/requirements/tests-2.6.txt b/requirements/tests-2.6.txt
index 6304339..29b771f 100644
--- a/requirements/tests-2.6.txt
+++ b/requirements/tests-2.6.txt
@@ -1,6 +1,6 @@
--requirement=base.txt
+--requirement=tests-base.txt
-jinja2
-mako
django==1.6
lamson
+ordereddict
diff --git a/requirements/tests-2.7.txt b/requirements/tests-2.7.txt
index ef61698..67385ef 100644
--- a/requirements/tests-2.7.txt
+++ b/requirements/tests-2.7.txt
@@ -1,6 +1,5 @@
--requirement=base.txt
+--requirement=tests-base.txt
-jinja2
-mako
django
-lamson
+lamson
\ No newline at end of file
diff --git a/requirements/tests-3.3.txt b/requirements/tests-3.3.txt
index cc678a8..b6d69fd 100644
--- a/requirements/tests-3.3.txt
+++ b/requirements/tests-3.3.txt
@@ -1,5 +1,4 @@
--requirement=base.txt
+--requirement=tests-base.txt
-jinja2
-mako
-django
+django
\ No newline at end of file
diff --git a/requirements/tests-3.4.txt b/requirements/tests-3.4.txt
index cc678a8..b6d69fd 100644
--- a/requirements/tests-3.4.txt
+++ b/requirements/tests-3.4.txt
@@ -1,5 +1,4 @@
--requirement=base.txt
+--requirement=tests-base.txt
-jinja2
-mako
-django
+django
\ No newline at end of file
diff --git a/requirements/tests-base.txt b/requirements/tests-base.txt
new file mode 100644
index 0000000..e729ff8
--- /dev/null
+++ b/requirements/tests-base.txt
@@ -0,0 +1,3 @@
+jinja2
+mako
+pytest
\ No newline at end of file
diff --git a/scripts/make_rfc822.py b/scripts/make_rfc822.py
index f4bbebf..a66452d 100755
--- a/scripts/make_rfc822.py
+++ b/scripts/make_rfc822.py
@@ -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
@@ -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()
diff --git a/setup.py b/setup.py
index 6498ea5..994a477 100644
--- a/setup.py
+++ b/setup.py
@@ -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,
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..b365588
--- /dev/null
+++ b/tox.ini
@@ -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