525 lines
17 KiB
Python
525 lines
17 KiB
Python
# w.c.s. - web application for online forms
|
|
# Copyright (C) 2005-2022 Entr'ouvert
|
|
#
|
|
# This program 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 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program 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 this program; if not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import collections.abc
|
|
|
|
from django.utils.timezone import now
|
|
from quixote import get_publisher
|
|
from quixote.html import htmltext
|
|
|
|
from wcs.mail_templates import MailTemplate
|
|
from wcs.qommon import _, emails
|
|
from wcs.qommon.errors import TooBigEmailError
|
|
from wcs.qommon.form import (
|
|
ComputedExpressionWidget,
|
|
SingleSelectWidget,
|
|
SingleSelectWidgetWithOther,
|
|
StringWidget,
|
|
TextWidget,
|
|
VarnameWidget,
|
|
WidgetList,
|
|
)
|
|
from wcs.qommon.misc import xml_node_text
|
|
from wcs.qommon.template import TemplateError
|
|
from wcs.variables import LazyList, LazyUser
|
|
from wcs.workflows import (
|
|
EvolutionPart,
|
|
WorkflowImportUnknownReferencedError,
|
|
WorkflowStatusItem,
|
|
get_role_translation_label,
|
|
register_item_class,
|
|
template_on_formdata,
|
|
)
|
|
|
|
|
|
class EmailEvolutionPart(EvolutionPart):
|
|
def __init__(self, varname, addresses, mail_subject, mail_body):
|
|
self.varname = varname
|
|
self.addresses = addresses
|
|
self.mail_subject = mail_subject
|
|
self.mail_body = mail_body
|
|
self.datetime = now()
|
|
|
|
|
|
class SendmailWorkflowStatusItem(WorkflowStatusItem):
|
|
description = _('Email')
|
|
key = 'sendmail'
|
|
category = 'interaction'
|
|
support_substitution_variables = True
|
|
|
|
to = []
|
|
subject = None
|
|
mail_template = None
|
|
body = None
|
|
custom_from = None
|
|
attachments = None
|
|
varname = None
|
|
|
|
comment = None
|
|
|
|
def get_dependencies(self):
|
|
yield from super().get_dependencies()
|
|
yield MailTemplate.get_by_slug(self.mail_template)
|
|
|
|
def _get_role_id_from_xml(self, elem, include_id=False, snapshot=False):
|
|
# override to allow for destination set with computed values.
|
|
if elem is None:
|
|
return None
|
|
value = xml_node_text(elem)
|
|
|
|
if self.get_expression(value)['type'] != 'text' or '@' in value:
|
|
return value
|
|
|
|
return super()._get_role_id_from_xml(elem, include_id=include_id, snapshot=snapshot)
|
|
|
|
def to_export_to_xml(self, item, include_id=False):
|
|
self._roles_export_to_xml('to', item, include_id=include_id, include_missing=True)
|
|
|
|
def mail_template_init_with_xml(self, elem, include_id=False, snapshot=False):
|
|
self.mail_template = None
|
|
if elem is None:
|
|
return
|
|
value = xml_node_text(elem)
|
|
if not value:
|
|
return
|
|
mail_template = MailTemplate.get_by_slug(value)
|
|
if not mail_template:
|
|
raise WorkflowImportUnknownReferencedError(
|
|
_('Unknown referenced mail template'), details={_('Unknown mail templates'): {value}}
|
|
)
|
|
self.mail_template = value
|
|
|
|
def render_list_of_roles_or_emails(self, roles):
|
|
t = []
|
|
for r in roles:
|
|
expression = self.get_expression(r)
|
|
if expression['type'] in ('python', 'template'):
|
|
t.append(_('computed value'))
|
|
elif '@' in expression['value']:
|
|
t.append(expression['value'])
|
|
else:
|
|
role_label = get_role_translation_label(self.get_workflow(), r)
|
|
if role_label:
|
|
t.append(role_label)
|
|
return ', '.join([str(x) for x in t])
|
|
|
|
def get_to_parameter_view_value(self):
|
|
return self.render_list_of_roles_or_emails(self.to)
|
|
|
|
def get_line_details(self):
|
|
if self.to:
|
|
return _('to %s') % self.render_list_of_roles_or_emails(self.to)
|
|
else:
|
|
return _('not completed')
|
|
|
|
def get_inspect_details(self):
|
|
if self.to:
|
|
return _('to %s') % self.render_list_of_roles_or_emails(self.to)
|
|
|
|
def get_parameters(self):
|
|
parameters = (
|
|
'to',
|
|
'mail_template',
|
|
'subject',
|
|
'body',
|
|
'varname',
|
|
'attachments',
|
|
'custom_from',
|
|
'condition',
|
|
)
|
|
if (
|
|
not get_publisher().has_site_option('include-sendmail-custom-from-option')
|
|
and not self.custom_from
|
|
):
|
|
parameters = tuple(x for x in parameters if x != 'custom_from')
|
|
return parameters
|
|
|
|
def get_inspect_parameters(self):
|
|
parameters = super().get_inspect_parameters()
|
|
if self.mail_template:
|
|
parameters.remove('subject')
|
|
parameters.remove('body')
|
|
return parameters
|
|
|
|
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs):
|
|
super().add_parameters_widgets(form, parameters, prefix=prefix, formdef=formdef, **kwargs)
|
|
subject_body_attrs = {}
|
|
if 'subject' in parameters or 'body' in parameters:
|
|
if MailTemplate.count():
|
|
subject_body_attrs = {
|
|
'data-dynamic-display-value': '',
|
|
'data-dynamic-display-child-of': '%smail_template' % prefix,
|
|
}
|
|
|
|
if 'to' in parameters:
|
|
form.add(
|
|
WidgetList,
|
|
'%sto' % prefix,
|
|
title=_('To'),
|
|
element_type=SingleSelectWidgetWithOther,
|
|
value=self.to,
|
|
add_element_label=self.get_add_role_label(),
|
|
element_kwargs={
|
|
'render_br': False,
|
|
'options': [(None, '---', None)] + self.get_list_of_roles(include_logged_in_users=False),
|
|
},
|
|
)
|
|
if 'subject' in parameters:
|
|
form.add(
|
|
StringWidget,
|
|
'%ssubject' % prefix,
|
|
title=_('Subject'),
|
|
validation_function=ComputedExpressionWidget.validate_template,
|
|
value=self.subject,
|
|
size=40,
|
|
attrs=subject_body_attrs,
|
|
)
|
|
if 'mail_template' in parameters and MailTemplate.count():
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'%smail_template' % prefix,
|
|
title=_('Mail Template'),
|
|
value=self.mail_template,
|
|
options=[(None, '', '')] + MailTemplate.get_as_options_list(),
|
|
attrs={'data-dynamic-display-parent': 'true'},
|
|
)
|
|
if 'body' in parameters:
|
|
form.add(
|
|
TextWidget,
|
|
'%sbody' % prefix,
|
|
title=_('Body'),
|
|
value=self.body,
|
|
cols=80,
|
|
rows=10,
|
|
validation_function=ComputedExpressionWidget.validate_template,
|
|
attrs=subject_body_attrs,
|
|
)
|
|
|
|
if 'varname' in parameters:
|
|
form.add(
|
|
VarnameWidget,
|
|
'%svarname' % prefix,
|
|
required=False,
|
|
title=_('Identifier'),
|
|
value=self.varname,
|
|
advanced=True,
|
|
hint=_('This is used to provide access to action details.'),
|
|
)
|
|
|
|
if 'custom_from' in parameters:
|
|
form.add(
|
|
ComputedExpressionWidget,
|
|
'%scustom_from' % prefix,
|
|
title=_('Custom From Address'),
|
|
value=self.custom_from,
|
|
advanced=True,
|
|
)
|
|
|
|
def get_body_parameter_view_value(self):
|
|
return htmltext('<pre class="wrapping-pre">%s</pre>') % self.body
|
|
|
|
def get_computed_strings(self):
|
|
yield from super().get_computed_strings()
|
|
if not self.mail_template:
|
|
yield self.subject
|
|
yield self.body
|
|
if self.to:
|
|
yield from self.to
|
|
yield from (self.attachments or [])
|
|
|
|
def perform(self, formdata, ignore_i18n=False):
|
|
if not self.to:
|
|
return
|
|
|
|
if self.mail_template:
|
|
mail_template = MailTemplate.get_by_slug(self.mail_template)
|
|
if mail_template:
|
|
body = mail_template.body
|
|
subject = mail_template.subject
|
|
extra_attachments = mail_template.attachments
|
|
else:
|
|
message = _('reference to invalid mail template %(mail_template)s in status %(status)s') % {
|
|
'status': self.parent.name,
|
|
'mail_template': self.mail_template,
|
|
}
|
|
get_publisher().record_error(message, formdata=formdata, status_item=self)
|
|
return
|
|
else:
|
|
body = self.body
|
|
subject = self.subject
|
|
extra_attachments = None
|
|
|
|
if not (subject and body):
|
|
return
|
|
|
|
if get_publisher().has_i18n_enabled() and not ignore_i18n:
|
|
orig_to = self.to
|
|
try:
|
|
submitter_language = formdata.get_submitter_language()
|
|
# send mail to submitter in submitter language
|
|
if submitter_language and '_submitter' in self.to:
|
|
with get_publisher().with_language(submitter_language):
|
|
to = self.to
|
|
self.to = ['_submitter']
|
|
self.perform(formdata, ignore_i18n=True)
|
|
self.to = [x for x in to if x != '_submitter']
|
|
# and others in the site language
|
|
with get_publisher().with_language(get_publisher().get_default_language()):
|
|
self.perform(formdata, ignore_i18n=True)
|
|
finally:
|
|
# restore attribute value
|
|
self.to = orig_to
|
|
return
|
|
|
|
subject = get_publisher().translate(subject)
|
|
body = get_publisher().translate(body)
|
|
|
|
try:
|
|
mail_body = template_on_formdata(
|
|
formdata, self.compute(body, render=False), autoescape=body.startswith('<')
|
|
)
|
|
except TemplateError as e:
|
|
get_publisher().record_error(
|
|
_('Error in body template, mail could not be generated'), formdata=formdata, exception=e
|
|
)
|
|
return
|
|
|
|
try:
|
|
mail_subject = template_on_formdata(
|
|
formdata, self.compute(subject, render=False), autoescape=False
|
|
)
|
|
except TemplateError as e:
|
|
get_publisher().record_error(
|
|
_('Error in subject template, mail could not be generated'), formdata=formdata, exception=e
|
|
)
|
|
return
|
|
|
|
# this works around the fact that parametric workflows only support
|
|
# string values, so if we get set a string, we convert it here to an
|
|
# array.
|
|
if isinstance(self.to, str):
|
|
self.to = [self.to]
|
|
|
|
dests = []
|
|
for dest in self.to:
|
|
with get_publisher().complex_data():
|
|
try:
|
|
dest = self.compute(dest, allow_complex=True, raises=True)
|
|
except Exception:
|
|
continue
|
|
else:
|
|
dest = get_publisher().get_cached_complex_data(dest)
|
|
|
|
if not dest:
|
|
continue
|
|
|
|
if isinstance(dest, (LazyUser, get_publisher().user_class)):
|
|
dests.append(dest)
|
|
continue
|
|
|
|
if not isinstance(dest, str):
|
|
if isinstance(dest, LazyList):
|
|
dest = list(dest)
|
|
if isinstance(dest, collections.abc.Iterable):
|
|
dests.extend(dest)
|
|
continue
|
|
|
|
if ',' in dest:
|
|
# if the email contains a comma consider it as a serie of
|
|
# emails
|
|
dests.extend([x.strip() for x in dest.split(',')])
|
|
continue
|
|
|
|
if '@' in dest:
|
|
dests.append(dest)
|
|
continue
|
|
|
|
if dest == '_submitter':
|
|
submitter_email = formdata.formdef.get_submitter_email(formdata)
|
|
if submitter_email:
|
|
dests.append(submitter_email)
|
|
continue
|
|
|
|
for real_dest in formdata.get_function_roles(dest):
|
|
if real_dest.startswith('_user:'):
|
|
try:
|
|
user = get_publisher().user_class.get(real_dest.split(':')[1])
|
|
except KeyError:
|
|
continue
|
|
dests.append(user)
|
|
continue
|
|
|
|
try:
|
|
role = get_publisher().role_class.get(real_dest)
|
|
except KeyError:
|
|
continue
|
|
dests.extend(role.get_emails())
|
|
|
|
addresses = set()
|
|
for value in dests:
|
|
if not value:
|
|
continue
|
|
if isinstance(value, LazyUser):
|
|
value = value._user
|
|
if isinstance(value, get_publisher().user_class):
|
|
if value.email:
|
|
addresses.add(value.email)
|
|
continue
|
|
addresses.add(value)
|
|
|
|
if not addresses:
|
|
return
|
|
|
|
email_from = None
|
|
if self.custom_from:
|
|
email_from = self.compute(self.custom_from)
|
|
|
|
attachments = self.convert_attachments_to_uploads(extra_attachments)
|
|
|
|
if self.varname:
|
|
formdata.evolution[-1].add_part(
|
|
EmailEvolutionPart(
|
|
varname=self.varname,
|
|
addresses=addresses,
|
|
mail_subject=mail_subject,
|
|
mail_body=mail_body,
|
|
)
|
|
)
|
|
formdata.store()
|
|
|
|
try:
|
|
if len(addresses) > 1:
|
|
email = emails.get_email(
|
|
mail_subject,
|
|
mail_body,
|
|
email_rcpt=None,
|
|
bcc=addresses,
|
|
email_from=email_from,
|
|
attachments=attachments,
|
|
)
|
|
else:
|
|
email = emails.get_email(
|
|
mail_subject,
|
|
mail_body,
|
|
email_rcpt=addresses,
|
|
email_from=email_from,
|
|
attachments=attachments,
|
|
)
|
|
|
|
if email:
|
|
self.send_email(email)
|
|
except TooBigEmailError:
|
|
get_publisher().record_error(_('Email too big to be sent'), formdata=formdata, status_item=self)
|
|
|
|
def send_email(self, email):
|
|
emails.send_email(email, fire_and_forget=True)
|
|
|
|
def i18n_scan(self, base_location):
|
|
location = '%sitems/%s/' % (base_location, self.id)
|
|
if not self.mail_template:
|
|
yield location, None, self.subject
|
|
yield location, None, self.body
|
|
|
|
def get_workflow_test_action(self, formdata, *args, **kwargs):
|
|
def record_email(email):
|
|
formdata.sent_emails.append(email)
|
|
|
|
setattr(self, 'send_email', record_email)
|
|
return self
|
|
|
|
|
|
register_item_class(SendmailWorkflowStatusItem)
|
|
|
|
|
|
class LazyFormDataEmailsBase:
|
|
def __init__(self, formdata):
|
|
self._formdata = formdata
|
|
|
|
def __getattr__(self, varname):
|
|
email_parts = []
|
|
for part in self._formdata.iter_evolution_parts(EmailEvolutionPart):
|
|
if part.varname == varname:
|
|
email_parts.append(LazyFormDataEmail(part))
|
|
if email_parts:
|
|
return LazyFormDataEmails(email_parts)
|
|
raise AttributeError(varname)
|
|
|
|
def inspect_keys(self):
|
|
varnames = set()
|
|
for part in self._formdata.iter_evolution_parts(EmailEvolutionPart):
|
|
if part.varname:
|
|
varnames.add(part.varname)
|
|
yield from varnames
|
|
|
|
|
|
class LazyFormDataEmails:
|
|
def __init__(self, email_parts):
|
|
self._email_parts = email_parts
|
|
|
|
def inspect_keys(self):
|
|
keys = self._email_parts[-1].inspect_keys()
|
|
if len(self._email_parts) > 1:
|
|
# if multiple emails with same varname have been sent, advertise
|
|
# access via indices.
|
|
keys.extend([str(x) for x in range(len(self._email_parts))])
|
|
return keys
|
|
|
|
def __getitem__(self, key):
|
|
try:
|
|
key = int(key)
|
|
except ValueError:
|
|
try:
|
|
return getattr(self, key)
|
|
except AttributeError:
|
|
return self._email_parts[-1][key]
|
|
return self._email_parts[key]
|
|
|
|
def __len__(self):
|
|
return len(self._email_parts)
|
|
|
|
def __iter__(self):
|
|
yield from self._email_parts
|
|
|
|
|
|
class LazyFormDataEmail:
|
|
def __init__(self, part):
|
|
self.part = part
|
|
|
|
def inspect_keys(self):
|
|
return ['addresses', 'body', 'subject', 'datetime']
|
|
|
|
def __getitem__(self, key):
|
|
try:
|
|
return getattr(self, key)
|
|
except AttributeError:
|
|
raise KeyError(key)
|
|
|
|
@property
|
|
def addresses(self):
|
|
return self.part.addresses
|
|
|
|
@property
|
|
def datetime(self):
|
|
return self.part.datetime
|
|
|
|
@property
|
|
def body(self):
|
|
return self.part.mail_body
|
|
|
|
@property
|
|
def subject(self):
|
|
return self.part.mail_subject
|