Changeset - 4c78223eb95e
[Not reviewed]
0 2 0
Lance Edgar (lance) - 5 years ago 2020-02-07 16:21:14
lance@edbob.org
Add new `ProblemReportEmail` base class, for simpler email previews

just so some of the template context can be provided automatically. although,
this *is* a bit heavy currently - should let caller pass in email handler etc.
2 files changed with 48 insertions and 2 deletions:
0 comments (0 inline, 0 general)
rattail/emails.py
Show inline comments
 
# -*- coding: utf-8; -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2019 Lance Edgar
 
#  Copyright © 2010-2020 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 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 General Public License for more
 
#  details.
 
#
 
#  You should have received a copy of the GNU General Public License along with
 
#  Rattail.  If not, see <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 
"""
 
Common email config objects
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import sys
 
import socket
 
from traceback import format_exception
 

	
 
import six
 

	
 
from rattail.mail import Email
 
from rattail.util import load_object
 
from rattail.core import Object
 
from rattail.time import make_utc, localtime
 
from rattail.problems import ProblemReport, get_problem_report_handler
 

	
 

	
 
class ProblemReportEmail(Email):
 
    """
 
    Base class for all "problem report" emails
 
    """
 
    abstract = True
 

	
 
    def obtain_sample_data(self, request):
 
        data = self.sample_data(request)
 
        handler = get_problem_report_handler(self.config)
 

	
 
        if 'report' not in data:
 
            reports = handler.get_all_problem_reports()
 
            email_key = self.__class__.__name__
 
            for report in reports:
 
                if report.email_key == email_key:
 
                    data['report'] = report(self.config)
 
                    break
 

	
 
            if 'report' not in data:
 
                report = ProblemReport(self.config)
 
                report.problem_title = "Generic Title (problem report not found)"
 
                data['report'] = report
 

	
 
        if 'system_title' not in data:
 
            system_key = data['report'].system_key or 'rattail'
 
            data['system_title'] = handler.get_system_title(system_key)
 

	
 
        return data
 

	
 

	
 
class datasync_error_watcher_get_changes(Email):
 
    """
 
    When any datasync watcher thread encounters an error trying to get changes,
 
    this email is sent out.
 
    """
 
    default_subject = "Watcher failed to get changes"
 

	
 
    def sample_data(self, request):
 
        from rattail.datasync import DataSyncWatcher
 
        try:
 
            raise RuntimeError("Fake error for preview")
 
        except:
 
            exc_type, exc, traceback = sys.exc_info()
 
        watcher = DataSyncWatcher(self.config, 'test')
 
        watcher.consumes_self = True
 
        return {
 
            'watcher': watcher,
 
            'error': exc,
 
            'traceback': ''.join(format_exception(exc_type, exc, traceback)).strip(),
 
            'datasync_url': '/datasyncchanges',
 
            'attempts': 2,
 
        }
 

	
 

	
 
class datasync_error_consumer_process_changes(Email):
 
    """
 
    When any datasync consumer thread encounters an error trying to process
 
    changes, this email is sent out.
 
    """
 
    default_subject = "Consumer failed to process changes"
 

	
 
    def sample_data(self, request):
 
        from rattail.datasync import DataSyncWatcher, DataSyncConsumer
 

	
 
        try:
 
            raise RuntimeError("Fake error for preview")
 
        except:
 
            exc_type, exc, traceback = sys.exc_info()
 

	
 
        watcher = DataSyncWatcher(self.config, 'testwatcher')
 
        consumer = DataSyncConsumer(self.config, 'testconsumer')
 
        return {
 
            'watcher': watcher,
 
            'consumer': consumer,
 
            'error': exc,
 
            'attempts': 2,
 
            'traceback': ''.join(format_exception(exc_type, exc, traceback)).strip(),
 
            'datasync_url': '/datasync/changes',
 
        }
 

	
 

	
 
class filemon_action_error(Email):
 
    """
 
    When any filemon thread encounters an error (and the retry attempts have
 
    been exhausted) then it will send out this email.
 
    """
 
    default_subject = "Error invoking action(s)"
 

	
 
    def sample_data(self, request):
 
        from rattail.filemon import Action
 
        action = Action(self.config)
 
        action.spec = 'rattail.filemon.actions:Action'
 
        action.retry_delay = 10
 
        try:
 
            raise RuntimeError("Fake error for preview")
 
        except:
 
            exc_type, exc, traceback = sys.exc_info()
 
        return {
 
            'hostname': socket.gethostname(),
 
            'path': '/tmp/foo.csv',
 
            'action': action,
 
            'attempts': 3,
 
            'error': exc,
 
            'traceback': ''.join(format_exception(exc_type, exc, traceback)).strip(),
 
        }
 

	
 

	
 
class ImporterEmail(Email):
 
    """
 
    Sent when a "version catch-up" import is performed, which involves changes.
 
    """
 
    abstract = True
 
    fallback_key = 'rattail_import_updates'
 
    handler_spec = None
 

	
 
    def get_handler(self, config):
 
        return load_object(self.handler_spec)(config)
 

	
 
    def sample_data(self, request):
 
        handler = self.get_handler(request.rattail_config)
 
        obj = Object()
 
        local_data = {
 
            'foo': 42,
 
            'bar': True,
 
            'baz': 'something',
 
        }
 
        host_data = {
 
            'foo': 42,
 
            'bar': False,
 
            'baz': 'something else',
 
        }
 
        return {
 
            'host_title': handler.host_title,
 
            'local_title': handler.local_title,
 
            'runtime': "1 second",
 
            'argv': ['bin/rattail', 'import-something'],
 
            'changes': {
 
                'Widget': (
 
                    [],                             # created
 
                    [(obj, local_data, host_data)], # updated
 
                    [],                             # deleted
 
                ),
 
            },
 
            'render_record': lambda x: six.text_type(x),
 
            'max_display': 15,
 
        }
 

	
 

	
 
class rattail_import_versions_updates(Email):
 
    """
 
    Sent when a "version catch-up" import is performed, which involves changes.
 
    """
 
    fallback_key = 'rattail_import_updates'
 
    default_subject = "Changes for Rattail -> Rattail (Versions) import"
 

	
 

	
 
class upgrade_failure(Email):
 
    """
 
    Sent when an app upgrade is attempted, but ultimately failed.
 
    """
 
    default_subject = "Upgrade Failure"
 

	
 
    def sample_data(self, request):
 
        upgrade = Object(
 
            description="upgrade to the latest!",
 
            notes="nothing special",
 
            executed=make_utc(),
 
            executed_by="Fred Flintstone",
 
            exit_code=42,
 
        )
 
        return {
 
            # TODO: first 3 args should be provided by some common logic instead...
 
            'rattail_config': self.config,
 
            'localtime': localtime,
 
            'app_title': self.config.app_title(),
 
            'upgrade': upgrade,
 
            'upgrade_url': '#',
 
        }
 

	
 

	
 
class upgrade_success(Email):
 
    """
 
    Sent when an app upgrade is performed successfully.
 
    """
 
    default_subject = "Upgrade Success"
 

	
 
    def sample_data(self, request):
 
        upgrade = Object(
 
            description="upgrade to the latest!",
 
            notes="nothing special",
 
            executed=make_utc(),
 
            executed_by="Fred Flintstone",
 
            exit_code=0,
 
        )
 
        return {
 
            # TODO: first 3 args should be provided by some common logic instead...
 
            'rattail_config': self.config,
 
            'localtime': localtime,
 
            'app_title': self.config.app_title(),
 
            'upgrade': upgrade,
 
            'upgrade_url': '#',
 
        }
 

	
 

	
 
class user_feedback(Email):
 
    """
 
    Sent when a user submits a Feedback form from the web UI.
 
    """
 
    default_subject = "User Feedback"
 

	
 
    def sample_data(self, request):
 
        return {
 
            'user': None,
 
            'user_name': "Fred Flintstone",
 
            'referrer': request.route_url('home'),
 
            'client_ip': '127.0.0.1',
 
            'message': "Hey there,\n\njust wondered what the heck was going on with this site?  It's crap.\n\nFred",
 
        }
rattail/mail.py
Show inline comments
 
# -*- coding: utf-8; -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2018 Lance Edgar
 
#  Copyright © 2010-2020 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 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 General Public License for more
 
#  details.
 
#
 
#  You should have received a copy of the GNU General Public License along with
 
#  Rattail.  If not, see <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 
"""
 
Email Framework
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import os
 
import smtplib
 
import logging
 
from email.charset import Charset
 
from email.message import Message
 
from email.mime.multipart import MIMEMultipart
 
from email.mime.application import MIMEApplication
 
from email.mime.text import MIMEText
 

	
 
import six
 

	
 
from mako.template import Template
 
from mako.lookup import TemplateLookup
 
from mako.exceptions import TopLevelLookupException
 

	
 
from rattail import exceptions
 
from rattail.core import UNSPECIFIED
 
from rattail.files import resource_path
 
from rattail.util import import_module_path
 
from rattail.time import localtime, make_utc
 

	
 

	
 
# NOTE: this bit of magic was stolen from Django
 
# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
 
# some spam filters.
 
utf8_charset = Charset('utf-8')
 
utf8_charset.body_encoding = None  # Python defaults to BASE64
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
def send_email(config, key, data={}, attachments=[],
 
               fallback_key=None, default_subject=None,
 
               enabled=UNSPECIFIED, **kwargs):
 
    """
 
    Send an email message of the given type, per config, with the given data
 
    and/or attachments.
 
    """
 
    # TODO: should let config override which handler we use
 
    handler = EmailHandler(config)
 
    email = handler.get_email(key, fallback_key=fallback_key,
 
                              default_subject=default_subject)
 

	
 
    if enabled is UNSPECIFIED:
 
        enabled = handler.get_enabled(email)
 

	
 
    if enabled:
 
        kwargs['attachments'] = attachments
 
        handler.send_message(email, data, **kwargs)
 
    else:
 
        log.debug("skipping email of type '%s' per config", key)
 

	
 

	
 
# TODO: deprecate / remove this (used only for tailbone preview?)
 
def deliver_message(config, key, msg, recipients=UNSPECIFIED):
 
    """
 
    Deliver an email message using the given SMTP configuration.
 
    """
 
    if recipients is UNSPECIFIED:
 
        recips = set()
 
        to = msg.get_all('To')
 
        if to:
 
            recips = recips.union(set(to))
 
        cc = msg.get_all('Cc')
 
        if cc:
 
            recips = recips.union(set(cc))
 
        bcc = msg.get_all('Bcc')
 
        if bcc:
 
            recips = recips.union(set(bcc))
 
    else:
 
        recips = set(recipients)
 
    if not recips:
 
        raise RuntimeError("No recipients for email: {0}".format(repr(msg)))
 

	
 
    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_feedback_only', usedb=False, default=False):
 
        send = key == 'user_feedback'
 
    else:
 
        send = config.getbool('rattail.mail', 'send_emails', usedb=False, default=True)
 

	
 
    if send:
 

	
 
        log.debug("attempting to send mail of type: %s", key)
 
        log.debug("connecting to server: %s", server)
 
        session = smtplib.SMTP(server)
 
        if username and password:
 
            result = session.login(username, password)
 
            log.debug("login() result is: %s", repr(result))
 

	
 
        result = session.sendmail(msg['From'], recips, msg.as_string())
 
        log.debug("sendmail() result is: %s", repr(result))
 
        session.quit()
 
        return True
 

	
 
    log.debug("config says no emails for '%s', but would have sent one to: %s", key, recips)
 
    return False
 

	
 

	
 
class EmailHandler(object):
 
    """
 
    Base class and default implementation for email handlers.
 
    """
 

	
 
    def __init__(self, config):
 
        self.config = config
 
        self.enum = self.config.get_enum()
 

	
 
    def get_email(self, key, fallback_key=None, **kwargs):
 
        """
 
        Return an email instance of the given type.
 
        """
 
        for email in self.iter_emails():
 
            if email.key == key or email.__name__ == key:
 
                return email(self.config, key, fallback_key, **kwargs)
 
        return Email(self.config, key, fallback_key, **kwargs)
 

	
 
    def iter_emails(self):
 
        """
 
        Iterate over all available email types.
 
        """
 
        for module in self.config.getlist('rattail.mail', 'emails', default=['rattail.emails']):
 
            module = import_module_path(module)
 
            for name in dir(module):
 
                obj = getattr(module, name)
 
                if (isinstance(obj, type) and issubclass(obj, Email)
 
                    and not obj.abstract and obj is not Email):
 
                    yield obj
 

	
 
    def get_enabled(self, email):
 
        return email.get_enabled()
 

	
 
    def send_message(self, email, data, **kwargs):
 
        msg = self.make_message(email, data, **kwargs)
 

	
 
        if self.config.getbool('rattail.mail', 'record_attempts', default=False):
 
            attempt = self.record_attempt(email, msg)
 
            try:
 
                self.deliver_message(email, msg)
 
            except Exception as e:
 
                self.record_failure(attempt, e)
 
            else:
 
                self.record_success(attempt)
 

	
 
        else: # don't record attempts
 
            self.deliver_message(email, msg)
 

	
 
    def record_attempt(self, email, msg):
 
        from rattail.db import Session, model
 

	
 
        session = Session()
 

	
 
        attempt = model.EmailAttempt()
 
        attempt.key = email.key
 
        attempt.sender = msg['From']
 
        attempt.to = msg.get_all('To')
 
        attempt.cc = msg.get_all('Cc')
 
        attempt.bcc = msg.get_all('Bcc')
 
        attempt.subject = msg['Subject']
 
        attempt.sent = make_utc()
 
        attempt.status_code = self.enum.EMAIL_ATTEMPT_CREATED
 

	
 
        session.add(attempt)
 
        session.commit()
 
        session.close()
 
        # session.expunge(attempt)
 
        return attempt
 

	
 
    def record_failure(self, attempt, error):
 
        from rattail.db import Session
 

	
 
        session = Session()
 
        attempt = session.merge(attempt)
 
        attempt.status_code = self.enum.EMAIL_ATTEMPT_FAILURE
 
        attempt.status_text = six.text_type(error)
 
        session.commit()
 
        session.close()
 

	
 
    def record_success(self, attempt):
 
        from rattail.db import Session
 

	
 
        session = Session()
 
        attempt = session.merge(attempt)
 
        attempt.status_code = self.enum.EMAIL_ATTEMPT_SUCCESS
 
        session.commit()
 
        session.close()
 

	
 
    def make_message(self, email, data, **kwargs):
 
        context = self.make_context(**data)
 
        return email.make_message(context, **kwargs)
 

	
 
    def make_context(self, **context):
 
        context['rattail_config'] = self.config
 
        context['app_title'] = self.config.app_title(default="Rattail")
 
        context['localtime'] = localtime
 
        return context
 

	
 
    def deliver_message(self, email, msg, recipients=UNSPECIFIED):
 
        """
 
        Deliver an email message using the given SMTP configuration.
 
        """
 
        if recipients is UNSPECIFIED:
 
            recips = set()
 
            to = msg.get_all('To')
 
            if to:
 
                recips = recips.union(set(to))
 
            cc = msg.get_all('Cc')
 
            if cc:
 
                recips = recips.union(set(cc))
 
            bcc = msg.get_all('Bcc')
 
            if bcc:
 
                recips = recips.union(set(bcc))
 
        else:
 
            recips = set(recipients)
 
        if not recips:
 
            raise RuntimeError("No recipients for email: {0}".format(repr(msg)))
 

	
 
        server = self.config.get('rattail.mail', 'smtp.server', default='localhost')
 
        username = self.config.get('rattail.mail', 'smtp.username')
 
        password = self.config.get('rattail.mail', 'smtp.password')
 

	
 
        if self.config.getbool('rattail.mail', 'send_feedback_only', usedb=False, default=False):
 
            send = email.key == 'user_feedback'
 
        else:
 
            send = self.config.getbool('rattail.mail', 'send_emails', usedb=False, default=True)
 

	
 
        if send:
 

	
 
            log.debug("attempting to send mail of type: %s", email.key)
 
            log.debug("connecting to server: %s", server)
 
            session = smtplib.SMTP(server)
 
            if username and password:
 
                result = session.login(username, password)
 
                log.debug("login() result is: %s", repr(result))
 

	
 
            result = session.sendmail(msg['From'], recips, msg.as_string())
 
            log.debug("sendmail() result is: %s", repr(result))
 
            session.quit()
 
            return True
 

	
 
        log.debug("config says no emails for '%s', but would have sent one to: %s", email.key, recips)
 
        return False
 

	
 

	
 
class Email(object):
 
    # Note: The docstring of an email is leveraged by code, hence this odd one.
 
    """
 
    (This email has no description.)
 
    """
 
    key = None
 
    fallback_key = None
 
    abstract = False
 
    default_prefix = "[rattail]"
 
    default_subject = "Automated message"
 

	
 
    # Whether or not the email's :attr:`to` attribute is dynamically determined
 
    # at run-time, i.e. via some logic other than typical reading from config.
 
    dynamic_to = False
 
    dynamic_to_help = None
 

	
 
    def __init__(self, config, key=None, fallback_key=None, default_subject=None):
 
        self.config = config
 
        self.enum = config.get_enum()
 

	
 
        if key:
 
            self.key = key
 
        elif not self.key:
 
            self.key = self.__class__.__name__
 
            if self.key == 'Email':
 
                raise exceptions.ConfigurationError("Email instance has no key: {0}".format(repr(self)))
 

	
 
        if fallback_key:
 
            self.fallback_key = fallback_key
 
        if default_subject:
 
            self.default_subject = default_subject
 

	
 
        templates = config.getlist('rattail.mail', 'templates')
 
        if templates:
 
            templates = [resource_path(p) for p in templates]
 
        self.templates = TemplateLookup(directories=templates)
 

	
 
    def obtain_sample_data(self, request):
 
        """
 
        This method is responsible for obtaining the full set of sample data,
 
        to be used as context when generating a preview for the email.
 

	
 
        Note, you normally should not override this method!  Please see also
 
        the :meth:`sample_data()` method.
 
        """
 
        return self.sample_data(request)
 

	
 
    def sample_data(self, request):
 
        """
 
        This method can return a dict of sample data, to be used as context
 
        when generating a preview for the email.  Subclasses are welcome to
 
        override this method.
 
        """
 
        return {}
 

	
 
    def get_enabled(self):
 
        """
 
        Get the enabled flag for the email's message type.
 
        """
 
        enabled = self.config.getbool('rattail.mail', '{0}.enabled'.format(self.key))
 
        if enabled is not None:
 
            return enabled
 
        enabled = self.config.getbool('rattail.mail', 'default.enabled')
 
        if enabled is not None:
 
            return enabled
 
        return self.config.getbool('rattail.mail', 'send_emails', default=True)
 

	
 
    def get_sender(self):
 
        """
 
        Returns the value for the message's ``From:`` header.
 

	
 
        :rtype: str
 
        """
 
        sender = self.config.get('rattail.mail', '{0}.from'.format(self.key))
 
        if not sender:
 
            sender = self.config.get('rattail.mail', 'default.from')
 
            if not sender:
 
                raise exceptions.SenderNotFound(self.key)
 
        return sender
 

	
 
    def get_replyto(self):
 
        """
 
        Get the Reply-To address for the message.
 
        """
 
        replyto = self.config.get('rattail.mail', '{0}.replyto'.format(self.key))
 
        if not replyto:
 
            replyto = self.config.get('rattail.mail', 'default.replyto')
 
        return replyto
 

	
 
    def get_recips(self, type_='to'):
 
        """
 
        Returns a list of recipients of the given type for the message.
 

	
 
        :param type_: Must be one of: ``('to', 'cc', 'bcc')``.
 

	
 
        :rtype: list
 
        """
 
        try:
 
            if type_.lower() not in ('to', 'cc', 'bcc'):
 
                raise Exception
 
        except:
 
            raise ValueError("Recipient type must be one of ('to', 'cc', 'bcc'); "
 
                             "not: {0}".format(repr(type_)))
 
        type_ = type_.lower()
 
        recips = self.config.getlist('rattail.mail', '{0}.{1}'.format(self.key, type_))
 
        if not recips:
 
            recips = self.config.getlist('rattail.mail', 'default.{0}'.format(type_))
 
        return recips
 

	
 
    def get_prefix(self, data={}, magic=True):
 
        """
 
        Returns a string to be used as the subject prefix for the message.
 

	
 
        :rtype: str
 
        """
 
        prefix = self.config.get('rattail.mail', '{0}.prefix'.format(self.key))
 
        if not prefix:
 
            prefix = self.config.get('rattail.mail', 'default.prefix')
 
        prefix = prefix or self.default_prefix
 
        if magic and not self.config.production():
 
            prefix = "[STAGE] {}".format(prefix)
 
        return prefix
 

	
 
    def get_subject(self, data={}, render=True, template=UNSPECIFIED):
 
        """
 
        Returns the base value for the message's subject header, i.e. minus
 
        prefix.
 

	
 
        :rtype: str
 
        """
 
        if template is UNSPECIFIED:
 
            template = self.config.get('rattail.mail', '{}.subject'.format(self.key),
 
                                       default=self.default_subject)
 
            if not template:
 
                template = self.config.get('rattail.mail', 'default.subject')
 
        if template and render:
 
            return Template(template).render(**data)
 
        return template
 

	
 
    def get_complete_subject(self, data={}, render=True, prefix=UNSPECIFIED, template=UNSPECIFIED):
 
        """
 
        Returns the value for the message's ``Subject:`` header, i.e. the base
 
        subject with the prefix applied.  Note that config may provide the
 
        complete subject also, in which case the prefix and base subject are
 
        not considered.
 

	
 
        :rtype: str
 
        """
 
        if prefix is UNSPECIFIED:
 
            prefix = self.get_prefix(data)
 
        prefix = (prefix or "").rstrip()
 
        if prefix:
 
            prefix = "{} ".format(prefix)
 
        return "{}{}".format(prefix, self.get_subject(data, render=render, template=template))
 

	
 
    def get_template(self, type_):
 
        """
 
        Locate and return the Mako email template of the given type
 
        (e.g. 'html'), or ``None`` if no such template can be found.
 
        """
 
        try:
 
            return self.templates.get_template('{0}.{1}.mako'.format(self.key, type_))
 
        except TopLevelLookupException:
 
            if self.fallback_key:
 
                try:
 
                    return self.templates.get_template('{0}.{1}.mako'.format(self.fallback_key, type_))
 
                except TopLevelLookupException:
 
                    pass
 

	
 
    def normalize_attachments(self, attachments):
 
        normalized = []
 
        for attachment in attachments:
 
            if isinstance(attachment, six.string_types):
 
                attachment = self.normalize_attachment(attachment)
 
            normalized.append(attachment)
 
        return normalized
 

	
 
    ATTACHMENT_MIME_MAP = {
 
        '.doc': 'application/msword',
 
        '.pdf': 'pdf',
 
        '.xls': 'vnd.ms-excel',
 
    }
 

	
 
    def normalize_attachment(self, path):
 
        root, ext = os.path.splitext(path)
 
        ext = ext.lower()
 
        if ext == '.csv':
 
            with open(path, 'rb') as f:
 
                part = MIMEText(f.read(), 'csv', 'utf_8')
 
            filename = os.path.basename(path)
 
            part.add_header('Content-Disposition', 'attachment; filename="{}"'.format(filename))
 
            return part
 
        else:
 
            mimetype = self.ATTACHMENT_MIME_MAP.get(ext)
 
            if mimetype:
 
                with open(path, 'rb') as f:
 
                    part = MIMEApplication(f.read(), mimetype)
 
                filename = os.path.basename(path)
 
                part.add_header('Content-Disposition', 'attachment; filename="{}"'.format(filename))
 
                return part
 
        raise ValueError("Magic is not (yet) supported, please construct your own attachments for file: {}".format(path))
 

	
 
    def make_message(self, data={}, attachments=[], inlines=[],
 
                     subject_prefix=UNSPECIFIED, subject_template=UNSPECIFIED,
 
                     sender=UNSPECIFIED, replyto=UNSPECIFIED,
 
                     to=UNSPECIFIED, cc=UNSPECIFIED, bcc=UNSPECIFIED):
 
        """
 
        Returns a proper email ``Message`` instance which may be sent via SMTP.
 
        """
 
        txt_template = self.get_template('txt')
 
        html_template = self.get_template('html')
 
        attachments = self.normalize_attachments(attachments)
 

	
 
        # TODO: provide more defaults?
 
        data.setdefault('six', six)
 

	
 
        if txt_template and html_template:
 

	
 
            txt_part = MIMEText(txt_template.render(**data), _charset='utf_8')
 

	
 
            html_part = MIMEText(html_template.render(**data), _subtype='html', _charset='utf_8')
 
            if inlines:
 
                html_part = MIMEMultipart(_subtype='related', _subparts=[html_part] + inlines)
 

	
 
            msg = MIMEMultipart(_subtype='alternative', _subparts=[txt_part, html_part])
 
            if attachments:
 
                msg = MIMEMultipart(_subtype='mixed', _subparts=[msg] + attachments)
 

	
 
        elif txt_template:
 

	
 
            msg = MIMEText(txt_template.render(**data), _charset='utf_8')
 
            if attachments:
 
                msg = MIMEMultipart(_subtype='mixed', _subparts=[msg] + attachments)
 

	
 
        elif html_template:
 

	
 
            msg = SafeMIMEText(html_template.render(**data), 'html', utf8_charset)
 
            if inlines:
 
                msg = MIMEMultipart(_subtype='related', _subparts=[msg] + inlines)
 
            if attachments:
 
                msg = MIMEMultipart(_subtype='mixed', _subparts=[msg] + attachments)
 

	
 
        else:
 
            raise exceptions.MailTemplateNotFound(self.key)
 

	
 
        self.add_headers(msg, data=data,
 
                         subject_prefix=subject_prefix, subject_template=subject_template,
 
                         sender=sender, replyto=replyto, to=to, cc=cc, bcc=bcc)
 
        return msg
 

	
 
    def add_headers(self, msg, data={},
 
                    subject_prefix=UNSPECIFIED, subject_template=UNSPECIFIED,
 
                    sender=UNSPECIFIED, replyto=UNSPECIFIED,
 
                    to=UNSPECIFIED, cc=UNSPECIFIED, bcc=UNSPECIFIED):
 
        """
 
        Adds headers for to/from addresses etc. to message
 
        """
 
        # subject/from
 
        msg['Subject'] = self.get_complete_subject(data, prefix=subject_prefix,
 
                                                   template=subject_template)
 
        if sender is UNSPECIFIED:
 
            sender = self.get_sender()
 
        msg['From'] = sender
 

	
 
        # reply-to
 
        if replyto is UNSPECIFIED:
 
            replyto = self.get_replyto()
 
        if replyto:
 
            msg.add_header('Reply-To', replyto)
 

	
 
        # recipients
 
        force_to = self.config.getlist('rattail.mail', 'force_to', usedb=False)
 
        if force_to:
 
            to = force_to
 
            cc = None
 
            bcc = None
 
        else:
 
            if to is UNSPECIFIED:
 
                to = self.get_recips('to')
 
            if cc is UNSPECIFIED:
 
                cc = self.get_recips('cc')
 
            if bcc is UNSPECIFIED:
 
                bcc = self.get_recips('bcc')
 
        if not (to or cc or bcc):
 
            raise exceptions.RecipientsNotFound(self.key)
 
        if to:
 
            for recip in to:
 
                msg['To'] = recip
 
        if cc:
 
            for recip in cc:
 
                msg['Cc'] = recip
 
        if bcc:
 
            for recip in bcc:
 
                msg['Bcc'] = recip
 

	
 

	
 
# NOTE: this bit of magic was stolen from Django
 
class SafeMIMEText(MIMEText):
 

	
 
    def __init__(self, text, subtype, charset):
 
        self.encoding = charset
 
        if charset == 'utf-8':
 
            # Unfortunately, Python doesn't support setting a Charset instance
 
            # as MIMEText init parameter (http://bugs.python.org/issue16324).
 
            # We do it manually and trigger re-encoding of the payload.
 
            MIMEText.__init__(self, text, subtype, None)
 
            del self['Content-Transfer-Encoding']
 
            # TODO: i don't personally need this yet, if ever?
 
            # # Workaround for versions without http://bugs.python.org/issue19063
 
            # if (3, 2) < sys.version_info < (3, 3, 4):
 
            #     payload = text.encode(utf8_charset.output_charset)
 
            #     self._payload = payload.decode('ascii', 'surrogateescape')
 
            #     self.set_charset(utf8_charset)
 
            # else:
 
            #     self.set_payload(text, utf8_charset)
 
            self.set_payload(text, utf8_charset)
 
            self.replace_header('Content-Type', 'text/%s; charset="%s"' % (subtype, charset))
 
        else:
 
            MIMEText.__init__(self, text, subtype, charset)
0 comments (0 inline, 0 general)