# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2015 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Email Framework
"""
from __future__ import absolute_import
from __future__ import unicode_literals
import smtplib
import warnings
import logging
from email.message import Message
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from mako.lookup import TemplateLookup
from mako.exceptions import TopLevelLookupException
from rattail import exceptions
from rattail.files import resource_path
log = logging.getLogger(__name__)
def send_message(config, sender, recipients, subject, body, content_type='text/plain'):
"""
Assemble and deliver an email message using the given parameters and configuration.
"""
message = make_message(sender, recipients, subject, body, content_type=content_type)
deliver_message(config, message)
def make_message(sender, recipients, subject, body, content_type='text/plain'):
"""
Assemble an email message object using the given parameters.
"""
message = Message()
message.set_type(content_type)
message['From'] = sender
for recipient in recipients:
message['To'] = recipient
message['Subject'] = subject
message.set_payload(body)
return message
def deliver_message(config, message):
"""
Deliver an email message using the given SMTP configuration.
"""
server = config.get('rattail.mail', 'smtp.server', default='localhost')
username = config.get('rattail.mail', 'smtp.username')
password = config.get('rattail.mail', 'smtp.password')
if config.getbool('rattail.mail', 'send_emails', default=True):
log.debug("connecting to server: {0}".format(server))
session = smtplib.SMTP(server)
if username and password:
result = session.login(username, password)
log.debug("deliver_message: login result is: {0}".format(repr(result)))
result = session.sendmail(message['From'], message.get_all('To'), message.as_string())
log.debug("deliver_message: sendmail result is: {0}".format(repr(result)))
session.quit()
else:
log.debug("config says no emails, but would have sent one to: {0}".format(
message.get_all('To')))
[docs]def send_email(config, key, data={}, subject=None, recipients=None, attachments=None,
finalize=None, template_key=None, fallback_key=None):
"""
Send an email message using configuration, exclusively.
Assuming a key of ``'foo'``, this should require something like:
.. code-block:: ini
[rattail.mail]
# second line overrides first, just a plain ol' Mako search path
templates =
rattail:templates/email
myproject:templates/email
foo.subject = [Rattail] Foo Alert
foo.from = rattail@example.com
foo.to =
general-manager@examle.com
store-manager@example.com
foo.cc =
department-heads@example.com
foo.bcc =
admin@example.com
And, the following templates should exist, say in ``rattail``:
* ``rattail/templates/email/foo.txt.mako``
* ``rattail/templates/email/foo.html.mako``
The ``data`` parameter will be passed directly to the template object(s).
The implementation should look for available template names and react
accordingly, e.g. if only a plain text is provided then the message will
not be multi-part at all (unless an attachment(s) requires it). However if
both templates are provided then the message will include both parts.
.. TODO: Flesh out the attachments idea, or perhaps implement finalize only as
.. it is the most generic? It would need to be a callback which receives the
.. actual message object which has been constructed thus far. It would then
.. have to return the message object after it had done "other things" to it.
.. TODO: The attachments idea on the other hand, should allow for a more
.. declarative (and therefore simpler) approach for the perhaps common case of
.. just needing to attach a file with a given name and type, etc. Probably
.. this should be a simple thing and not require one to specify a callback.
"""
if not get_enabled(config, key, fallback_key):
log.debug("skipping email of type '{0}' per config".format(key))
return
try:
template = get_template(config, template_key or key)
except TopLevelLookupException:
if fallback_key:
try:
template = get_template(config, fallback_key)
except TopLevelLookupException:
# reattempt first template, so error makes more sense
get_template(config, template_key or key)
else:
raise
body = template.render(**data)
message = make_message_config(config, key, body, subject=subject,
recipients=recipients,
attachments=attachments,
fallback_key=fallback_key)
deliver_message(config, message)
def get_template(config, key, type_='html'):
"""
Locate and return the email template corresponding to the provided key. No
attempt is made to confirm its existence etc., this just lets the Mako
logic do its thing.
"""
templates = config.getlist('rattail.mail', 'templates')
templates = [resource_path(p) for p in templates]
lookup = TemplateLookup(directories=templates)
return lookup.get_template('{0}.{1}.mako'.format(key, type_))
def make_message_config(config, key, body, subject=None, recipients=None,
replyto=None, attachments=None, fallback_key=None):
"""
Assemble an email message using configuration, exclusively.
Assuming a key of ``'foo'``, this should require something like:
.. code-block:: ini
[rattail.mail]
foo.subject = [Rattail] Foo Alert
foo.from = rattail@example.com
foo.to =
general-manager@examle.com
store-manager@example.com
foo.cc =
department-heads@example.com
foo.bcc =
admin@example.com
"""
if attachments is not None:
message = MIMEMultipart()
message.attach(MIMEText(body))
for attachment in attachments:
message.attach(attachment)
else:
message = Message()
message.set_payload(body, 'utf_8')
message.set_type('text/html')
message['From'] = get_sender(config, key, fallback_key)
if replyto is None:
replyto = get_replyto(config, key, fallback_key)
if replyto:
message.add_header('Reply-To', replyto)
if recipients is None:
recipients = get_recipients(config, key, fallback_key)
for recipient in recipients:
message['To'] = recipient
if subject is None:
subject = get_subject(config, key, fallback_key)
message['Subject'] = subject
return message
def get_enabled(config, key, fallback_key=None):
"""
Get the enabled flag for an email message.
"""
enabled = config.getbool('rattail.mail', '{0}.enabled'.format(key))
if enabled is not None:
return enabled
if fallback_key:
enabled = config.get('rattail.mail', '{0}.enabled'.format(fallback_key))
if enabled is not None:
return enabled
enabled = config.getbool('rattail.mail', 'default.enabled')
if enabled is not None:
return enabled
return config.getbool('rattail.mail', 'send_emails', default=True)
def get_sender(config, key, fallback_key=None):
"""
Get the sender (From:) address for an email message.
"""
sender = config.get('rattail.mail', '{0}.from'.format(key))
if sender:
return sender
if fallback_key:
sender = config.get('rattail.mail', '{0}.from'.format(fallback_key))
if sender:
return sender
sender = config.get('rattail.mail', 'default.from')
if sender:
return sender
raise exceptions.SenderNotFound(key)
def get_replyto(config, key, fallback_key=None):
"""
Get the Reply-To address for an email message.
"""
replyto = config.get('rattail.mail', '{0}.replyto'.format(key))
if replyto:
return replyto
if fallback_key:
replyto = config.get('rattail.mail', '{0}.replyto'.format(fallback_key))
if replyto:
return replyto
replyto = config.get('rattail.mail', 'default.replyto')
if replyto:
return replyto
def get_recipients(config, key, fallback_key=None):
"""
Get the list of recipients (To:) addresses for an email message.
"""
recipients = config.getlist('rattail.mail', '{0}.to'.format(key))
if recipients:
return recipients
if fallback_key:
recipients = config.getlist('rattail.mail', '{0}.to'.format(fallback_key))
if recipients:
return recipients
recipients = config.getlist('rattail.mail', 'default.to')
if recipients:
return recipients
raise exceptions.RecipientsNotFound(key)
def get_subject(config, key, fallback_key=None):
"""
Get the subject for an email message.
"""
subject = config.get('rattail.mail', '{0}.subject'.format(key))
if subject:
return subject
if fallback_key:
subject = config.get('rattail.mail', '{0}.subject'.format(fallback_key))
if subject:
return subject
subject = config.get('rattail.mail', 'default.subject')
if subject:
return subject
# Fall back to a sane default.
return "[Rattail] Automated Message"