433 lines
13 KiB
Python
433 lines
13 KiB
Python
# -*- coding: utf-8; -*-
|
|
################################################################################
|
|
#
|
|
# Rattail -- Retail Software Framework
|
|
# Copyright © 2010-2022 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 Views
|
|
"""
|
|
|
|
from __future__ import unicode_literals, absolute_import
|
|
|
|
import re
|
|
|
|
import six
|
|
|
|
from rattail import mail
|
|
from rattail.db import api, model
|
|
from rattail.config import parse_list
|
|
|
|
import colander
|
|
from deform import widget as dfwidget
|
|
from webhelpers2.html import HTML
|
|
|
|
from tailbone.db import Session
|
|
from tailbone.views import View, MasterView
|
|
|
|
|
|
class EmailSettingView(MasterView):
|
|
"""
|
|
Master view for email admin (settings/preview).
|
|
"""
|
|
normalized_model_name = 'emailprofile'
|
|
model_title = "Email Setting"
|
|
model_key = 'key'
|
|
url_prefix = '/settings/email'
|
|
filterable = False
|
|
pageable = False
|
|
creatable = False
|
|
deletable = False
|
|
configurable = True
|
|
config_title = "Email"
|
|
|
|
grid_columns = [
|
|
'key',
|
|
'prefix',
|
|
'subject',
|
|
'to',
|
|
'enabled',
|
|
]
|
|
|
|
form_fields = [
|
|
'key',
|
|
'fallback_key',
|
|
'description',
|
|
'prefix',
|
|
'subject',
|
|
'sender',
|
|
'replyto',
|
|
'to',
|
|
'cc',
|
|
'bcc',
|
|
'enabled',
|
|
]
|
|
|
|
def __init__(self, request):
|
|
super(EmailSettingView, self).__init__(request)
|
|
self.handler = self.get_handler()
|
|
|
|
def get_handler(self):
|
|
app = self.get_rattail_app()
|
|
return app.get_email_handler()
|
|
|
|
def get_data(self, session=None):
|
|
data = []
|
|
for email in self.handler.iter_emails():
|
|
key = email.key or email.__name__
|
|
email = email(self.rattail_config, key)
|
|
data.append(self.normalize(email))
|
|
return data
|
|
|
|
def configure_grid(self, g):
|
|
g.sorters['key'] = g.make_simple_sorter('key', foldcase=True)
|
|
g.sorters['prefix'] = g.make_simple_sorter('prefix', foldcase=True)
|
|
g.sorters['subject'] = g.make_simple_sorter('subject', foldcase=True)
|
|
g.sorters['enabled'] = g.make_simple_sorter('enabled')
|
|
g.set_sort_defaults('key')
|
|
g.set_type('enabled', 'boolean')
|
|
g.set_link('key')
|
|
g.set_link('subject')
|
|
|
|
# to
|
|
g.set_renderer('to', self.render_to_short)
|
|
g.sorters['to'] = g.make_simple_sorter('to', foldcase=True)
|
|
|
|
def render_to_short(self, email, column):
|
|
profile = email['_email']
|
|
if self.rattail_config.production():
|
|
if profile.dynamic_to:
|
|
if profile.dynamic_to_help:
|
|
return profile.dynamic_to_help
|
|
|
|
value = email['to']
|
|
if not value:
|
|
return ""
|
|
recips = parse_list(value)
|
|
if len(recips) < 3:
|
|
return value
|
|
return "{}, ...".format(', '.join(recips[:2]))
|
|
|
|
def normalize(self, email):
|
|
def get_recips(type_):
|
|
recips = email.get_recips(type_)
|
|
if recips:
|
|
return ', '.join(recips)
|
|
data = email.obtain_sample_data(self.request)
|
|
return {
|
|
'_email': email,
|
|
'key': email.key,
|
|
'fallback_key': email.fallback_key,
|
|
'description': email.__doc__,
|
|
'prefix': email.get_prefix(data, magic=False) or '',
|
|
'subject': email.get_subject(data, render=False) or '',
|
|
'sender': email.get_sender() or '',
|
|
'replyto': email.get_replyto() or '',
|
|
'to': get_recips('to') or '',
|
|
'cc': get_recips('cc') or '',
|
|
'bcc': get_recips('bcc') or '',
|
|
'enabled': email.get_enabled(),
|
|
}
|
|
|
|
def get_instance(self):
|
|
key = self.request.matchdict['key']
|
|
return self.normalize(self.handler.get_email(key))
|
|
|
|
def get_instance_title(self, email):
|
|
return email['_email'].get_complete_subject(render=False)
|
|
|
|
def editable_instance(self, profile):
|
|
if self.rattail_config.demo():
|
|
return profile['key'] != 'user_feedback'
|
|
return True
|
|
|
|
def deletable_instance(self, profile):
|
|
if self.rattail_config.demo():
|
|
return profile['key'] != 'user_feedback'
|
|
return True
|
|
|
|
def configure_form(self, f):
|
|
super(EmailSettingView, self).configure_form(f)
|
|
profile = f.model_instance['_email']
|
|
|
|
# key
|
|
f.set_readonly('key')
|
|
|
|
# fallback_key
|
|
f.set_readonly('fallback_key')
|
|
|
|
# description
|
|
f.set_readonly('description')
|
|
|
|
# prefix
|
|
f.set_label('prefix', "Subject Prefix")
|
|
|
|
# subject
|
|
f.set_label('subject', "Subject Text")
|
|
|
|
# sender
|
|
f.set_label('sender', "From")
|
|
|
|
# replyto
|
|
f.set_label('replyto', "Reply-To")
|
|
|
|
# to
|
|
f.set_widget('to', dfwidget.TextAreaWidget(cols=60, rows=6))
|
|
if self.rattail_config.production():
|
|
if profile.dynamic_to:
|
|
f.set_readonly('to')
|
|
if profile.dynamic_to_help:
|
|
f.model_instance['to'] = profile.dynamic_to_help
|
|
|
|
# cc
|
|
f.set_widget('cc', dfwidget.TextAreaWidget(cols=60, rows=2))
|
|
|
|
# bcc
|
|
f.set_widget('bcc', dfwidget.TextAreaWidget(cols=60, rows=2))
|
|
|
|
# enabled
|
|
f.set_type('enabled', 'boolean')
|
|
|
|
def make_form_schema(self):
|
|
return EmailProfileSchema()
|
|
|
|
def save_edit_form(self, form):
|
|
key = self.request.matchdict['key']
|
|
data = self.form_deserialized
|
|
session = self.Session()
|
|
api.save_setting(session, 'rattail.mail.{}.prefix'.format(key), data['prefix'])
|
|
api.save_setting(session, 'rattail.mail.{}.subject'.format(key), data['subject'])
|
|
api.save_setting(session, 'rattail.mail.{}.from'.format(key), data['sender'])
|
|
api.save_setting(session, 'rattail.mail.{}.replyto'.format(key), data['replyto'])
|
|
api.save_setting(session, 'rattail.mail.{}.to'.format(key), (data['to'] or '').replace('\n', ', '))
|
|
api.save_setting(session, 'rattail.mail.{}.cc'.format(key), (data['cc'] or '').replace('\n', ', '))
|
|
api.save_setting(session, 'rattail.mail.{}.bcc'.format(key), (data['bcc'] or '').replace('\n', ', '))
|
|
api.save_setting(session, 'rattail.mail.{}.enabled'.format(key), six.text_type(data['enabled']).lower())
|
|
return data
|
|
|
|
def template_kwargs_view(self, **kwargs):
|
|
key = self.request.matchdict['key']
|
|
kwargs['email'] = self.handler.get_email(key)
|
|
return kwargs
|
|
|
|
def configure_get_simple_settings(self):
|
|
config = self.rattail_config
|
|
return [
|
|
|
|
# sending
|
|
{'section': 'rattail.mail',
|
|
'option': 'record_attempts',
|
|
'type': bool},
|
|
]
|
|
|
|
# TODO: deprecate / remove this
|
|
ProfilesView = EmailSettingView
|
|
|
|
|
|
class RecipientsType(colander.String):
|
|
"""
|
|
Custom schema type for email recipients. This is used to present the
|
|
recipients as a "list" within the text area, i.e. one recipient per line.
|
|
Then the list is collapsed to a comma-delimited string for storage.
|
|
"""
|
|
|
|
def serialize(self, node, appstruct):
|
|
if appstruct is colander.null:
|
|
return colander.null
|
|
recips = parse_list(appstruct)
|
|
return '\n'.join(recips)
|
|
|
|
def deserialize(self, node, cstruct):
|
|
if cstruct == '' and self.allow_empty:
|
|
return ''
|
|
if not cstruct:
|
|
return colander.null
|
|
recips = parse_list(cstruct)
|
|
return ', '.join(recips)
|
|
|
|
|
|
class EmailProfileSchema(colander.MappingSchema):
|
|
|
|
prefix = colander.SchemaNode(colander.String())
|
|
|
|
subject = colander.SchemaNode(colander.String())
|
|
|
|
sender = colander.SchemaNode(colander.String())
|
|
|
|
replyto = colander.SchemaNode(colander.String(), missing='')
|
|
|
|
to = colander.SchemaNode(RecipientsType())
|
|
|
|
cc = colander.SchemaNode(RecipientsType(), missing='')
|
|
|
|
bcc = colander.SchemaNode(RecipientsType(), missing='')
|
|
|
|
enabled = colander.SchemaNode(colander.Boolean())
|
|
|
|
|
|
class EmailPreview(View):
|
|
"""
|
|
Lists available email templates, and can show previews of each.
|
|
"""
|
|
|
|
def __init__(self, request):
|
|
super(EmailPreview, self).__init__(request)
|
|
self.handler = self.get_handler()
|
|
|
|
def get_handler(self):
|
|
app = self.get_rattail_app()
|
|
return app.get_email_handler()
|
|
|
|
def __call__(self):
|
|
|
|
# Forms submitted via POST are only used for sending emails.
|
|
if self.request.method == 'POST':
|
|
self.email_template()
|
|
url = self.request.get_referrer(default=self.request.route_url('emailprofiles'))
|
|
return self.redirect(url)
|
|
|
|
# Maybe render a preview?
|
|
key = self.request.GET.get('key')
|
|
if key:
|
|
type_ = self.request.GET.get('type', 'html')
|
|
return self.preview_template(key, type_)
|
|
|
|
assert False, "should not be here"
|
|
|
|
def email_template(self):
|
|
recipient = self.request.POST.get('recipient')
|
|
if recipient:
|
|
key = self.request.POST.get('email_key')
|
|
if key:
|
|
email = self.handler.get_email(key)
|
|
data = email.obtain_sample_data(self.request)
|
|
|
|
self.handler.send_message(email, data,
|
|
subject_prefix="[PREVIEW] ",
|
|
to=[recipient],
|
|
cc=None, bcc=None)
|
|
|
|
self.request.session.flash(
|
|
"Preview for '{}' was emailed to {}".format(
|
|
key, recipient))
|
|
|
|
def preview_template(self, key, type_):
|
|
email = self.handler.get_email(key)
|
|
template = email.get_template(type_)
|
|
data = email.obtain_sample_data(self.request)
|
|
self.request.response.text = template.render(**data)
|
|
if type_ == 'txt':
|
|
self.request.response.content_type = str('text/plain')
|
|
return self.request.response
|
|
|
|
@classmethod
|
|
def defaults(cls, config):
|
|
# email preview
|
|
config.add_route('email.preview', '/email/preview/')
|
|
config.add_view(cls, route_name='email.preview',
|
|
renderer='/email/preview.mako',
|
|
permission='emailprofiles.preview')
|
|
config.add_tailbone_permission('emailprofiles', 'emailprofiles.preview',
|
|
"Send preview email")
|
|
|
|
|
|
class EmailAttemptView(MasterView):
|
|
"""
|
|
Master view for email attempts.
|
|
"""
|
|
model_class = model.EmailAttempt
|
|
route_prefix = 'email_attempts'
|
|
url_prefix = '/email/attempts'
|
|
creatable = False
|
|
editable = False
|
|
deletable = False
|
|
|
|
labels = {
|
|
'status_code': "Status",
|
|
}
|
|
|
|
grid_columns = [
|
|
'key',
|
|
'sender',
|
|
'subject',
|
|
'to',
|
|
'sent',
|
|
'status_code',
|
|
]
|
|
|
|
form_fields = [
|
|
'key',
|
|
'sender',
|
|
'subject',
|
|
'to',
|
|
'cc',
|
|
'bcc',
|
|
'sent',
|
|
'status_code',
|
|
'status_text',
|
|
]
|
|
|
|
def configure_grid(self, g):
|
|
super(EmailAttemptView, self).configure_grid(g)
|
|
|
|
# sent
|
|
g.set_sort_defaults('sent', 'desc')
|
|
|
|
# status_code
|
|
g.set_enum('status_code', self.enum.EMAIL_ATTEMPT)
|
|
|
|
# to
|
|
g.set_renderer('to', self.render_to_short)
|
|
|
|
# links
|
|
g.set_link('key')
|
|
g.set_link('sender')
|
|
g.set_link('subject')
|
|
g.set_link('to')
|
|
|
|
to_pattern = re.compile(r'^\{(.*)\}$')
|
|
|
|
def render_to_short(self, attempt, column):
|
|
value = attempt.to
|
|
if not value:
|
|
return
|
|
|
|
match = self.to_pattern.match(value)
|
|
if match:
|
|
recips = parse_list(match.group(1))
|
|
if len(recips) > 2:
|
|
recips = recips[:2]
|
|
recips.append('...')
|
|
recips = [HTML.escape(r) for r in recips]
|
|
return ', '.join(recips)
|
|
|
|
return value
|
|
|
|
def configure_form(self, f):
|
|
super(EmailAttemptView, self).configure_form(f)
|
|
|
|
# status_code
|
|
f.set_enum('status_code', self.enum.EMAIL_ATTEMPT)
|
|
|
|
|
|
def includeme(config):
|
|
EmailSettingView.defaults(config)
|
|
EmailPreview.defaults(config)
|
|
EmailAttemptView.defaults(config)
|