Permalink
Browse files

Move to new flask extension structure and remove Lamson dependency in…

… favor of smtplib
  • Loading branch information...
1 parent 50be773 commit 932f2ca81a148bc412391f277fbe3b1a7e38b4a3 @mattupstate committed Jun 12, 2012
Showing with 431 additions and 442 deletions.
  1. +27 −0 .gitignore
  2. +0 −29 .hgignore
  3. +397 −0 flask_mail.py
  4. +0 −1 flaskext/__init__.py
  5. +0 −126 flaskext/mail/__init__.py
  6. +0 −91 flaskext/mail/connection.py
  7. +0 −171 flaskext/mail/message.py
  8. +0 −14 flaskext/mail/signals.py
  9. +3 −6 setup.py
  10. +4 −4 tests.py
View
27 .gitignore
@@ -0,0 +1,27 @@
+*.py[co]
+
+# Packages
+*.egg
+*.egg-info
+dist
+*build
+eggs
+parts
+bin
+var
+sdist
+develop-eggs
+.installed.cfg
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+
+#Translations
+*.mo
+
+#Mr Developer
+.mr.developer.cfg
View
29 .hgignore
@@ -1,29 +0,0 @@
-syntax: glob
-
-*.coverage
-*.egg-info
-*.egg
-*.log
-*.pyc
-*.db
-*.swp
-*.swo
-*.zip
-*.orig
-*.cfg
-*.tox
-
-
-*~
-
-fab_settings.py
-production_settings.py
-
-dist
-build
-docs/output
-
-_uploads
-
-.git
-.gitignore
View
397 flask_mail.py
@@ -0,0 +1,397 @@
+# -*- coding: utf-8 -*-
+"""
+ flaskext.mail
+ ~~~~~~~~~~~~~
+
+ Flask extension for sending email.
+
+ :copyright: (c) 2010 by Dan Jacob.
+ :license: BSD, see LICENSE for more details.
+"""
+from __future__ import with_statement
+
+import blinker
+import smtplib
+import socket
+
+from email.encoders import encode_base64
+from email.mime.base import MIMEBase
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from contextlib import contextmanager
+
+from flask import current_app
+
+
+class Connection(object):
+
+ """Handles connection to host."""
+
+ def __init__(self, mail, max_emails=None):
+
+ self.mail = mail
+ self.app = self.mail.app
+ self.suppress = self.mail.suppress
+ self.max_emails = max_emails or self.mail.max_emails or 0
+ self.fail_silently = self.mail.fail_silently
+
+ def __enter__(self):
+
+ if self.suppress:
+ self.host = None
+ else:
+ self.host = self.configure_host()
+
+ self.num_emails = 0
+
+ return self
+
+ def __exit__(self, exc_type, exc_value, tb):
+ if self.host:
+ self.host.quit()
+
+ def configure_host(self):
+
+ try:
+ if self.mail.use_ssl:
+ host = smtplib.SMTP_SSL(self.mail.server, self.mail.port)
+ else:
+ host = smtplib.SMTP(self.mail.server, self.mail.port)
+ except socket.error:
+ if self.fail_silently:
+ return
+ raise
+
+ host.set_debuglevel(int(self.app.debug))
+
+ if self.mail.use_tls:
+ host.starttls()
+ if self.mail.username and self.mail.password:
+ host.login(self.mail.username, self.mail.password)
+
+ return host
+
+ def send(self, message):
+ """
+ Sends message.
+
+ :param message: Message instance.
+ """
+
+ if self.host:
+ self.host.sendmail(message.sender,
+ message.send_to,
+ message.as_string())
+
+ if email_dispatched:
+ email_dispatched.send(message, app=self.app)
+
+ self.num_emails += 1
+
+ if self.num_emails == self.max_emails:
+
+ self.num_emails = 0
+ if self.host:
+ self.host.quit()
+ self.host = self.configure_host()
+
+ def send_message(self, *args, **kwargs):
+ """
+ Shortcut for send(msg).
+
+ Takes same arguments as Message constructor.
+
+ :versionadded: 0.3.5
+
+ """
+
+ self.send(Message(*args, **kwargs))
+
+
+class BadHeaderError(Exception):
+ pass
+
+
+class Attachment(object):
+
+ """
+ Encapsulates file attachment information.
+
+ :versionadded: 0.3.5
+
+ :param filename: filename of attachment
+ :param content_type: file mimetype
+ :param data: the raw file data
+ :param disposition: content-disposition (if any)
+
+ """
+
+ def __init__(self, filename=None, content_type=None, data=None,
+ disposition=None):
+
+ self.filename = filename
+ self.content_type = content_type
+ self.data = data
+ self.disposition = disposition or 'attachment'
+
+
+class Message(object):
+
+ """
+ Encapsulates an email message.
+
+ :param subject: email subject header
+ :param recipients: list of email addresses
+ :param body: plain text message
+ :param html: HTML message
+ :param sender: email sender address, or **DEFAULT_MAIL_SENDER** by default
+ :param cc: CC list
+ :param bcc: BCC list
+ :param attachments: list of Attachment instances
+ :param reply_to: reply-to address
+ """
+
+ def __init__(self, subject,
+ recipients=None,
+ body=None,
+ html=None,
+ sender=None,
+ cc=None,
+ bcc=None,
+ attachments=None,
+ reply_to=None):
+
+ if sender is None:
+ sender = current_app.config.get("DEFAULT_MAIL_SENDER")
+
+ if isinstance(sender, tuple):
+ # sender can be tuple of (name, address)
+ sender = "%s <%s>" % sender
+
+ self.subject = subject
+ self.sender = sender
+ self.body = body
+ self.html = html
+
+ self.cc = cc
+ self.bcc = bcc
+ self.reply_to = reply_to
+
+ if recipients is None:
+ recipients = []
+
+ self.recipients = list(recipients)
+
+ if attachments is None:
+ attachments = []
+
+ self.attachments = attachments
+
+ @property
+ def send_to(self):
+ return set(self.recipients) | set(self.bcc or ()) | set(self.cc or ())
+
+ def as_string(self):
+ """
+ Creates the email
+ """
+ msg = MIMEMultipart('alternative')
+
+ msg['Subject'] = self.subject
+ msg['From'] = self.sender
+ msg['To'] = ', '.join(self.recipients)
+
+ if self.bcc:
+ msg['Bcc'] = ', '.join(self.bcc)
+
+ if self.cc:
+ msg['Cc'] = ', '.join(self.cc)
+
+ if self.reply_to:
+ msg['Reply-To'] = self.reply_to
+
+ part1 = MIMEText(self.body, 'plain')
+ part2 = MIMEText(self.html, 'html')
+
+ msg.attach(part1)
+ msg.attach(part2)
+
+ for attachment in self.attachments:
+ f = MIMEBase(*attachment.content_type.split('/'))
+ f.set_payload(attachment.data)
+ encode_base64(f)
+ f.add_header('Content-Disposition', '%s;filename=%s' %
+ (attachment.disposition, attachment.filename))
+ msg.attach(f)
+
+ return msg.as_string()
+
+ def is_bad_headers(self):
+ """
+ Checks for bad headers i.e. newlines in subject, sender or recipients.
+ """
+
+ reply_to = self.reply_to or ''
+ for val in [self.subject, self.sender, reply_to] + self.recipients:
+ for c in '\r\n':
+ if c in val:
+ return True
+ return False
+
+ def send(self, connection):
+ """
+ Verifies and sends the message.
+ """
+
+ assert self.recipients, "No recipients have been added"
+ assert self.body or self.html, "No body or HTML has been set"
+ assert self.sender, "No sender address has been set"
+
+ if self.is_bad_headers():
+ raise BadHeaderError
+
+ connection.send(self)
+
+ def add_recipient(self, recipient):
+ """
+ Adds another recipient to the message.
+
+ :param recipient: email address of recipient.
+ """
+
+ self.recipients.append(recipient)
+
+ def attach(self,
+ filename=None,
+ content_type=None,
+ data=None,
+ disposition=None):
+
+ """
+ Adds an attachment to the message.
+
+ :param filename: filename of attachment
+ :param content_type: file mimetype
+ :param data: the raw file data
+ :param disposition: content-disposition (if any)
+ """
+
+ self.attachments.append(
+ Attachment(filename, content_type, data, disposition))
+
+
+class Mail(object):
+ """
+ Manages email messaging
+
+ :param app: Flask instance
+ """
+
+ def __init__(self, app=None):
+
+ if app is not None:
+ self.init_app(app)
+
+ def init_app(self, app):
+ """
+ Initializes your mail settings from the application
+ settings.
+
+ You can use this if you want to set up your Mail instance
+ at configuration time.
+
+ :param app: Flask application instance
+ """
+
+ self.server = app.config.get('MAIL_SERVER', '127.0.0.1')
+ self.username = app.config.get('MAIL_USERNAME')
+ self.password = app.config.get('MAIL_PASSWORD')
+ self.port = app.config.get('MAIL_PORT', 25)
+ self.use_tls = app.config.get('MAIL_USE_TLS', False)
+ self.use_ssl = app.config.get('MAIL_USE_SSL', False)
+ self.debug = int(app.config.get('MAIL_DEBUG', app.debug))
+ self.max_emails = app.config.get('DEFAULT_MAX_EMAILS')
+ self.suppress = app.config.get('MAIL_SUPPRESS_SEND', False)
+ self.fail_silently = app.config.get('MAIL_FAIL_SILENTLY', True)
+
+ self.suppress = self.suppress or app.testing
+ self.app = app
+
+ # register extension with app
+ app.extensions = getattr(app, 'extensions', {})
+ app.extensions['mail'] = self
+
+
+ @contextmanager
+ def record_messages(self):
+ """
+ Records all messages. Use in unit tests for example::
+
+ with mail.record_messages() as outbox:
+ response = app.test_client.get("/email-sending-view/")
+ assert len(outbox) == 1
+ assert outbox[0].subject == "testing"
+
+ You must have blinker installed in order to use this feature.
+ :versionadded: 0.4
+ """
+
+ if not email_dispatched:
+ raise RuntimeError, "blinker must be installed"
+
+ outbox = []
+
+ def _record(message, app):
+ outbox.append(message)
+
+ email_dispatched.connect(_record)
+
+ try:
+ yield outbox
+ finally:
+ email_dispatched.disconnect(_record)
+
+ def send(self, message):
+ """
+ Sends a single message instance. If TESTING is True
+ the message will not actually be sent.
+
+ :param message: a Message instance.
+ """
+
+ with self.connect() as connection:
+ message.send(connection)
+
+ def send_message(self, *args, **kwargs):
+ """
+ Shortcut for send(msg).
+
+ Takes same arguments as Message constructor.
+
+ :versionadded: 0.3.5
+ """
+
+ self.send(Message(*args, **kwargs))
+
+ def connect(self, max_emails=None):
+ """
+ Opens a connection to the mail host.
+
+ :param max_emails: the maximum number of emails that can
+ be sent in a single connection. If this
+ number is exceeded the Connection instance
+ will reconnect to the mail server. The
+ DEFAULT_MAX_EMAILS config setting is used
+ if this is None.
+ """
+ return Connection(self, max_emails)
+
+
+signals = blinker.Namespace()
+
+email_dispatched = signals.signal("email-dispatched", doc="""
+Signal sent when an email is dispatched. This signal will also be sent
+in testing mode, even though the email will not actually be sent.
+""")
+
+
View
1 flaskext/__init__.py
@@ -1 +0,0 @@
-__import__('pkg_resources').declare_namespace(__name__)
View
126 flaskext/mail/__init__.py
@@ -1,126 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
- flaskext.mail
- ~~~~~~~~~~~~~
-
- Flask extension for sending email.
-
- :copyright: (c) 2010 by Dan Jacob.
- :license: BSD, see LICENSE for more details.
-"""
-from __future__ import with_statement
-
-from contextlib import contextmanager
-
-from flaskext.mail.connection import Connection
-from flaskext.mail.message import Message, Attachment, BadHeaderError
-from flaskext.mail.signals import email_dispatched
-
-
-class Mail(object):
- """
- Manages email messaging
-
- :param app: Flask instance
- """
-
- def __init__(self, app=None):
-
- if app is not None:
- self.init_app(app)
-
- def init_app(self, app):
- """
- Initializes your mail settings from the application
- settings.
-
- You can use this if you want to set up your Mail instance
- at configuration time.
-
- :param app: Flask application instance
- """
-
- self.server = app.config.get('MAIL_SERVER', '127.0.0.1')
- self.username = app.config.get('MAIL_USERNAME')
- self.password = app.config.get('MAIL_PASSWORD')
- self.port = app.config.get('MAIL_PORT', 25)
- self.use_tls = app.config.get('MAIL_USE_TLS', False)
- self.use_ssl = app.config.get('MAIL_USE_SSL', False)
- self.debug = int(app.config.get('MAIL_DEBUG', app.debug))
- self.max_emails = app.config.get('DEFAULT_MAX_EMAILS')
- self.suppress = app.config.get('MAIL_SUPPRESS_SEND', False)
- self.fail_silently = app.config.get('MAIL_FAIL_SILENTLY', True)
-
- self.suppress = self.suppress or app.testing
- self.app = app
-
- # register extension with app
- app.extensions = getattr(app, 'extensions', {})
- app.extensions['mail'] = self
-
-
- @contextmanager
- def record_messages(self):
- """
- Records all messages. Use in unit tests for example::
-
- with mail.record_messages() as outbox:
- response = app.test_client.get("/email-sending-view/")
- assert len(outbox) == 1
- assert outbox[0].subject == "testing"
-
- You must have blinker installed in order to use this feature.
- :versionadded: 0.4
- """
-
- if not email_dispatched:
- raise RuntimeError, "blinker must be installed"
-
- outbox = []
-
- def _record(message, app):
- outbox.append(message)
-
- email_dispatched.connect(_record)
-
- try:
- yield outbox
- finally:
- email_dispatched.disconnect(_record)
-
- def send(self, message):
- """
- Sends a single message instance. If TESTING is True
- the message will not actually be sent.
-
- :param message: a Message instance.
- """
-
- with self.connect() as connection:
- message.send(connection)
-
- def send_message(self, *args, **kwargs):
- """
- Shortcut for send(msg).
-
- Takes same arguments as Message constructor.
-
- :versionadded: 0.3.5
- """
-
- self.send(Message(*args, **kwargs))
-
- def connect(self, max_emails=None):
- """
- Opens a connection to the mail host.
-
- :param max_emails: the maximum number of emails that can
- be sent in a single connection. If this
- number is exceeded the Connection instance
- will reconnect to the mail server. The
- DEFAULT_MAX_EMAILS config setting is used
- if this is None.
- """
- return Connection(self, max_emails)
-
-
View
91 flaskext/mail/connection.py
@@ -1,91 +0,0 @@
-import smtplib
-import socket
-
-from flaskext.mail.message import Message
-from flaskext.mail.signals import email_dispatched
-
-class Connection(object):
-
- """Handles connection to host."""
-
- def __init__(self, mail, max_emails=None):
-
- self.mail = mail
- self.app = self.mail.app
- self.suppress = self.mail.suppress
- self.max_emails = max_emails or self.mail.max_emails or 0
- self.fail_silently = self.mail.fail_silently
-
- def __enter__(self):
-
- if self.suppress:
- self.host = None
- else:
- self.host = self.configure_host()
-
- self.num_emails = 0
-
- return self
-
- def __exit__(self, exc_type, exc_value, tb):
- if self.host:
- self.host.quit()
-
- def configure_host(self):
-
- try:
- if self.mail.use_ssl:
- host = smtplib.SMTP_SSL(self.mail.server, self.mail.port)
- else:
- host = smtplib.SMTP(self.mail.server, self.mail.port)
- except socket.error:
- if self.fail_silently:
- return
- raise
-
- host.set_debuglevel(int(self.app.debug))
-
- if self.mail.use_tls:
- host.starttls()
- if self.mail.username and self.mail.password:
- host.login(self.mail.username, self.mail.password)
-
- return host
-
- def send(self, message):
- """
- Sends message.
-
- :param message: Message instance.
- """
-
- if self.host:
- self.host.sendmail(message.sender,
- message.send_to,
- str(message.get_response()))
-
- if email_dispatched:
- email_dispatched.send(message, app=self.app)
-
- self.num_emails += 1
-
- if self.num_emails == self.max_emails:
-
- self.num_emails = 0
- if self.host:
- self.host.quit()
- self.host = self.configure_host()
-
- def send_message(self, *args, **kwargs):
- """
- Shortcut for send(msg).
-
- Takes same arguments as Message constructor.
-
- :versionadded: 0.3.5
-
- """
-
- self.send(Message(*args, **kwargs))
-
-
View
171 flaskext/mail/message.py
@@ -1,171 +0,0 @@
-
-from flask import _request_ctx_stack
-
-from lamson.mail import MailResponse
-
-class BadHeaderError(Exception): pass
-
-
-class Attachment(object):
-
- """
- Encapsulates file attachment information.
-
- :versionadded: 0.3.5
-
- :param filename: filename of attachment
- :param content_type: file mimetype
- :param data: the raw file data
- :param disposition: content-disposition (if any)
-
- """
-
- def __init__(self, filename=None, content_type=None, data=None,
- disposition=None):
-
- self.filename = filename
- self.content_type = content_type
- self.data = data
- self.disposition = disposition or 'attachment'
-
-
-class Message(object):
-
- """
- Encapsulates an email message.
-
- :param subject: email subject header
- :param recipients: list of email addresses
- :param body: plain text message
- :param html: HTML message
- :param sender: email sender address, or **DEFAULT_MAIL_SENDER** by default
- :param cc: CC list
- :param bcc: BCC list
- :param attachments: list of Attachment instances
- :param reply_to: reply-to address
- """
-
- def __init__(self, subject,
- recipients=None,
- body=None,
- html=None,
- sender=None,
- cc=None,
- bcc=None,
- attachments=None,
- reply_to=None):
-
-
- if sender is None:
- app = _request_ctx_stack.top.app
- sender = app.config.get("DEFAULT_MAIL_SENDER")
-
- if isinstance(sender, tuple):
- # sender can be tuple of (name, address)
- sender = "%s <%s>" % sender
-
- self.subject = subject
- self.sender = sender
- self.body = body
- self.html = html
-
- self.cc = cc
- self.bcc = bcc
- self.reply_to = reply_to
-
- if recipients is None:
- recipients = []
-
- self.recipients = list(recipients)
-
- if attachments is None:
- attachments = []
-
- self.attachments = attachments
-
- @property
- def send_to(self):
- return set(self.recipients) | set(self.bcc or ()) | set(self.cc or ())
-
- def get_response(self):
- """
- Creates a Lamson MailResponse instance
- """
-
- response = MailResponse(Subject=self.subject,
- To=self.recipients,
- From=self.sender,
- Body=self.body,
- Html=self.html)
-
- if self.bcc:
- response.base['Bcc'] = self.bcc
-
- if self.cc:
- response.base['Cc'] = self.cc
-
- if self.reply_to:
- response.base['Reply-To'] = self.reply_to
-
- for attachment in self.attachments:
-
- response.attach(attachment.filename,
- attachment.content_type,
- attachment.data,
- attachment.disposition)
-
- return response
-
- def is_bad_headers(self):
- """
- Checks for bad headers i.e. newlines in subject, sender or recipients.
- """
-
- reply_to = self.reply_to or ''
- for val in [self.subject, self.sender, reply_to] + self.recipients:
- for c in '\r\n':
- if c in val:
- return True
- return False
-
- def send(self, connection):
- """
- Verifies and sends the message.
- """
-
- assert self.recipients, "No recipients have been added"
- assert self.body or self.html, "No body or HTML has been set"
- assert self.sender, "No sender address has been set"
-
- if self.is_bad_headers():
- raise BadHeaderError
-
- connection.send(self)
-
- def add_recipient(self, recipient):
- """
- Adds another recipient to the message.
-
- :param recipient: email address of recipient.
- """
-
- self.recipients.append(recipient)
-
- def attach(self,
- filename=None,
- content_type=None,
- data=None,
- disposition=None):
-
- """
- Adds an attachment to the message.
-
- :param filename: filename of attachment
- :param content_type: file mimetype
- :param data: the raw file data
- :param disposition: content-disposition (if any)
- """
-
- self.attachments.append(
- Attachment(filename, content_type, data, disposition))
-
View
14 flaskext/mail/signals.py
@@ -1,14 +0,0 @@
-
-try:
-
- import blinker
- signals = blinker.Namespace()
-
- email_dispatched = signals.signal("email-dispatched", doc="""
-Signal sent when an email is dispatched. This signal will also be sent
-in testing mode, even though the email will not actually be sent.
- """)
-
-except ImportError:
- email_dispatched = None
-
View
9 setup.py
@@ -18,24 +18,21 @@
setup(
name='Flask-Mail',
- version='0.6.1',
+ version='0.7.0',
url='http://bitbucket.org/danjac/flask-mail',
license='BSD',
author='Dan Jacob',
author_email='danjac354@gmail.com',
description='Flask extension for sending email',
long_description=__doc__,
- packages=[
- 'flaskext',
- 'flaskext.mail',
+ py_modules=[
+ 'flask_mail'
],
- namespace_packages=['flaskext'],
test_suite='nose.collector',
zip_safe=False,
platforms='any',
install_requires=[
'Flask',
- 'Lamson',
'blinker',
],
tests_require=[
View
8 tests.py
@@ -8,7 +8,7 @@
from email import encoders
from flask import Flask, g
-from flaskext.mail import Mail, Message, BadHeaderError, Attachment
+from flask_mail import Mail, Message, BadHeaderError, Attachment
class TestCase(unittest.TestCase):
@@ -80,7 +80,7 @@ def test_reply_to(self):
reply_to="somebody <somebody@example.com>",
body="testing")
- response = msg.get_response()
+ response = msg.as_string()
self.assertIn("Reply-To: somebody <somebody@example.com>", str(response))
def test_send_without_sender(self):
@@ -141,7 +141,7 @@ def test_bcc(self):
body="testing",
bcc=["tosomeoneelse@example.com"])
- response = msg.get_response()
+ response = msg.as_string()
self.assertIn("Bcc: tosomeoneelse@example.com", str(response))
def test_cc(self):
@@ -151,7 +151,7 @@ def test_cc(self):
body="testing",
cc=["tosomeoneelse@example.com"])
- response = msg.get_response()
+ response = msg.as_string()
self.assertIn("Cc: tosomeoneelse@example.com", str(response))
def test_attach(self):

0 comments on commit 932f2ca

Please sign in to comment.