Files
tryton/modules/notification_email/notification.py
T

353 lines
13 KiB
Python
Raw Normal View History

2017-06-24 10:51:20 +02:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import mimetypes
2024-01-25 16:04:05 +01:00
from email.utils import getaddresses
2017-06-24 10:51:20 +02:00
2021-12-11 17:58:42 +01:00
from genshi.template import TextTemplate
2020-07-04 20:07:54 +02:00
2025-10-07 08:19:57 +02:00
import trytond.config as config
2020-07-04 20:07:54 +02:00
from trytond.i18n import gettext
2021-12-11 17:58:42 +01:00
from trytond.model import ModelSQL, ModelView, fields
2017-06-24 10:51:20 +02:00
from trytond.pool import Pool
2022-11-24 19:16:51 +01:00
from trytond.pyson import Eval, TimeDelta
2017-08-28 23:15:47 +02:00
from trytond.report import get_email
2024-02-21 21:38:53 +01:00
from trytond.sendmail import SMTPDataManager, send_message_transactional
2025-06-15 12:19:39 +02:00
from trytond.tools import slugify
2024-02-21 21:43:50 +01:00
from trytond.tools.email_ import format_address, has_rcpt, set_from_header
2017-06-24 10:51:20 +02:00
from trytond.transaction import Transaction
2021-12-11 17:58:42 +01:00
from .exceptions import TemplateError
2020-08-11 22:00:26 +02:00
2017-06-24 10:51:20 +02:00
class Email(ModelSQL, ModelView):
__name__ = 'notification.email'
from_ = fields.Char(
"From", translate=True,
2017-06-24 10:51:20 +02:00
help="Leave empty for the value defined in the configuration file.")
2020-07-04 20:07:54 +02:00
subject = fields.Char(
"Subject", translate=True,
help="The Genshi syntax can be used "
"with 'record' in the evaluation context.\n"
"If empty the report name will be used.")
2017-06-24 10:51:20 +02:00
recipients = fields.Many2One(
'ir.model.field', "Recipients",
domain=[
2024-02-23 18:40:52 +01:00
('model', '=', Eval('model')),
2017-06-24 10:51:20 +02:00
],
help="The field that contains the recipient(s).")
2018-03-10 17:37:01 +01:00
fallback_recipients = fields.Many2One(
'res.user', "Recipients Fallback",
2018-03-10 17:37:01 +01:00
domain=[
('email', '!=', None),
],
2024-08-22 09:52:06 +02:00
help="User notified when no recipients email is found.")
2017-06-24 10:51:20 +02:00
recipients_secondary = fields.Many2One(
'ir.model.field', "Secondary Recipients",
domain=[
2024-02-23 18:40:52 +01:00
('model', '=', Eval('model')),
2017-06-24 10:51:20 +02:00
],
help="The field that contains the secondary recipient(s).")
2018-03-10 17:37:01 +01:00
fallback_recipients_secondary = fields.Many2One(
'res.user', "Secondary Recipients Fallback",
2018-03-10 17:37:01 +01:00
domain=[
('email', '!=', None),
],
2024-08-22 09:52:06 +02:00
help="User notified when no secondary recipients email is found.")
2017-06-24 10:51:20 +02:00
recipients_hidden = fields.Many2One(
'ir.model.field', "Hidden Recipients",
domain=[
2024-02-23 18:40:52 +01:00
('model', '=', Eval('model')),
2017-06-24 10:51:20 +02:00
],
help="The field that contains the hidden recipient(s).")
2018-03-10 17:37:01 +01:00
fallback_recipients_hidden = fields.Many2One(
'res.user', "Hidden Recipients Fallback",
2018-03-10 17:37:01 +01:00
domain=[
('email', '!=', None),
],
2024-08-22 09:52:06 +02:00
help="User notified when no hidden recipients email is found.")
2017-06-24 10:51:20 +02:00
2018-02-19 19:16:17 +01:00
contact_mechanism = fields.Selection(
'get_contact_mechanisms', "Contact Mechanism",
help="Define which email to use from the party's contact mechanisms")
2017-06-24 10:51:20 +02:00
content = fields.Many2One(
'ir.action.report', "Content", required=True,
2017-08-24 15:43:46 +02:00
domain=[('template_extension', 'in', ['txt', 'html', 'xhtml'])],
2017-06-24 10:51:20 +02:00
help="The report used as email template.")
2017-08-02 22:53:56 +02:00
attachments = fields.Many2Many(
'notification.email.attachment', 'notification', 'report',
"Attachments",
domain=[
('model', '=', Eval('model')),
],
help="The reports used as attachments.")
2017-06-24 10:51:20 +02:00
triggers = fields.One2Many(
'ir.trigger', 'notification_email', "Triggers",
2024-11-16 21:17:55 +01:00
domain=[('model.name', '=', Eval('model'))],
2017-06-24 10:51:20 +02:00
help="Add a trigger for the notification.")
2021-11-24 01:06:26 +01:00
send_after = fields.TimeDelta(
"Send After",
2022-11-24 19:16:51 +01:00
domain=['OR',
('send_after', '=', None),
('send_after', '>=', TimeDelta()),
],
2021-11-24 01:06:26 +01:00
help="The delay after which the email must be sent.\n"
"Applied if a worker queue is activated.")
2017-06-24 10:51:20 +02:00
model = fields.Function(
fields.Char("Model"), 'on_change_with_model', searcher='search_model')
2020-09-17 17:18:02 +02:00
@classmethod
def __setup__(cls):
pool = Pool()
EmailTemplate = pool.get('ir.email.template')
super().__setup__()
for field in [
'recipients',
'recipients_secondary',
'recipients_hidden',
]:
field = getattr(cls, field)
field.domain.append(['OR',
('relation', 'in', EmailTemplate.email_models()),
[
2024-11-16 21:17:55 +01:00
('model.name', 'in', EmailTemplate.email_models()),
2020-09-17 17:18:02 +02:00
('name', '=', 'id'),
],
])
2017-06-24 10:51:20 +02:00
def get_rec_name(self, name):
return self.content.rec_name
@classmethod
def search_rec_name(cls, name, clause):
return [('content',) + tuple(clause[1:])]
2018-02-19 19:16:17 +01:00
@classmethod
def get_contact_mechanisms(cls):
pool = Pool()
try:
ContactMechanism = pool.get('party.contact_mechanism')
except KeyError:
return [(None, "")]
return ContactMechanism.usages()
2017-06-24 10:51:20 +02:00
@fields.depends('content')
def on_change_with_model(self, name=None):
if self.content:
return self.content.model
@classmethod
def search_model(cls, name, clause):
return [('content.model',) + tuple(clause[1:])]
def _get_addresses(self, value):
pool = Pool()
2020-09-17 17:18:02 +02:00
EmailTemplate = pool.get('ir.email.template')
with Transaction().set_context(usage=self.contact_mechanism):
2024-02-21 21:43:50 +01:00
# reformat addresses with encoding
return [
format_address(e, n)
for n, e in getaddresses(EmailTemplate.get_addresses(value))]
2017-06-24 10:51:20 +02:00
def _get_languages(self, value):
2020-09-17 17:18:02 +02:00
pool = Pool()
EmailTemplate = pool.get('ir.email.template')
2021-04-11 18:37:41 +02:00
with Transaction().set_context(usage=self.contact_mechanism):
return EmailTemplate.get_languages(value)
2017-06-24 10:51:20 +02:00
2021-09-26 23:11:21 +02:00
def get_email(self, record, sender, to, cc, bcc, languages):
2017-08-02 22:53:56 +02:00
pool = Pool()
Attachment = pool.get('notification.email.attachment')
2017-08-28 23:15:47 +02:00
# TODO order languages to get default as last one for title
2024-01-25 16:04:05 +01:00
msg, title = get_email(self.content, record, languages)
2024-02-21 21:38:53 +01:00
if languages:
language = list(languages)[-1].code
else:
language = None
2021-09-26 23:11:21 +02:00
from_ = sender
2024-02-21 21:38:53 +01:00
with Transaction().set_context(language=language):
notification = self.__class__(self.id)
if notification.from_:
from_ = notification.from_
if self.subject:
2020-07-04 20:07:54 +02:00
title = (TextTemplate(notification.subject)
.generate(record=record)
.render())
2017-08-02 22:53:56 +02:00
if self.attachments:
for report in self.attachments:
2024-02-21 21:38:53 +01:00
name, data = Attachment.get(report, record, language)
2024-01-25 16:04:05 +01:00
ctype, _ = mimetypes.guess_type(name)
if not ctype:
ctype = 'application/octet-stream'
maintype, subtype = ctype.split('/', 1)
msg.add_attachment(
data,
maintype=maintype,
subtype=subtype,
filename=('utf-8', '', name))
2017-08-02 22:53:56 +02:00
2021-09-26 23:11:21 +02:00
set_from_header(msg, sender, from_)
if to:
msg['To'] = to
if cc:
msg['Cc'] = cc
if bcc:
msg['Bcc'] = bcc
2024-01-25 16:04:05 +01:00
msg['Subject'] = title
2017-08-02 22:52:12 +02:00
msg['Auto-Submitted'] = 'auto-generated'
2024-02-21 21:43:50 +01:00
return msg
2017-06-24 10:51:20 +02:00
@classmethod
def trigger(cls, records, trigger):
"Action function for the triggers"
2021-11-24 01:06:26 +01:00
notification_email = trigger.notification_email
if not notification_email:
2017-06-24 10:51:20 +02:00
raise ValueError(
'Trigger "%s" is not related to any email notification'
% trigger.rec_name)
2021-11-24 01:06:26 +01:00
if notification_email.send_after:
with Transaction().set_context(
queue_scheduled_at=trigger.notification_email.send_after):
notification_email.__class__.__queue__._send_email_queued(
notification_email, [r.id for r in records], trigger.id)
else:
notification_email.send_email(records, trigger)
def _send_email_queued(self, ids, trigger_id):
pool = Pool()
Model = pool.get(self.model)
Trigger = pool.get('ir.trigger')
records = Model.browse(ids)
trigger = Trigger(trigger_id)
self.send_email(records, trigger)
2017-06-24 10:51:20 +02:00
def send_email(self, records, trigger):
pool = Pool()
Email = pool.get('ir.email')
2017-06-24 10:51:20 +02:00
datamanager = SMTPDataManager()
Transaction().join(datamanager)
from_ = (config.get('notification_email', 'from')
or config.get('email', 'from'))
emails = []
2017-06-24 10:51:20 +02:00
for record in records:
2021-07-12 23:12:23 +02:00
to, to_languages = self._get_to(record)
cc, cc_languages = self._get_cc(record)
bcc, bcc_languages = self._get_bcc(record)
2023-03-27 16:11:19 +02:00
languages = to_languages | cc_languages | bcc_languages
2024-02-21 21:43:50 +01:00
msg = self.get_email(record, from_, to, cc, bcc, languages)
if has_rcpt(msg):
2024-02-21 21:38:53 +01:00
send_message_transactional(msg, datamanager=datamanager)
2024-02-21 21:43:50 +01:00
emails.append(Email.from_message(
msg, resource=record,
notification_email=trigger.notification_email,
notification_trigger=trigger))
Email.save(emails)
2017-06-24 10:51:20 +02:00
2022-02-18 13:17:45 +01:00
def _get_recipients(self, record, name):
if name == 'id':
return record
else:
return getattr(record, name, None)
2021-07-12 23:12:23 +02:00
def _get_to(self, record):
to = []
2023-03-27 16:11:19 +02:00
languages = set()
2021-07-12 23:12:23 +02:00
if self.recipients:
2022-02-18 13:17:45 +01:00
recipients = self._get_recipients(record, self.recipients.name)
2021-07-12 23:12:23 +02:00
if recipients:
2023-03-27 16:11:19 +02:00
languages.update(self._get_languages(recipients))
2021-07-12 23:12:23 +02:00
to = self._get_addresses(recipients)
if not to and self.fallback_recipients:
2023-03-27 16:11:19 +02:00
languages.update(
2021-07-12 23:12:23 +02:00
self._get_languages(self.fallback_recipients))
to = self._get_addresses(self.fallback_recipients)
2023-03-27 16:11:19 +02:00
return to, languages
2021-07-12 23:12:23 +02:00
def _get_cc(self, record):
cc = []
2023-03-27 16:11:19 +02:00
languages = set()
2021-07-12 23:12:23 +02:00
if self.recipients_secondary:
2022-02-18 13:17:45 +01:00
recipients_secondary = self._get_recipients(
record, self.recipients_secondary.name)
2021-07-12 23:12:23 +02:00
if recipients_secondary:
2023-03-27 16:11:19 +02:00
languages.update(
2021-07-12 23:12:23 +02:00
self._get_languages(recipients_secondary))
cc = self._get_addresses(recipients_secondary)
if not cc and self.fallback_recipients_secondary:
2023-03-27 16:11:19 +02:00
languages.update(
2021-07-12 23:12:23 +02:00
self._get_languages(self.fallback_recipients_secondary))
cc = self._get_addresses(self.fallback_recipients_secondary)
2023-03-27 16:11:19 +02:00
return cc, languages
2021-07-12 23:12:23 +02:00
def _get_bcc(self, record):
bcc = []
2023-03-27 16:11:19 +02:00
languages = set()
2021-07-12 23:12:23 +02:00
if self.recipients_hidden:
2022-02-18 13:17:45 +01:00
recipients_hidden = self._get_recipients(
record, self.recipients_hidden.name)
2021-07-12 23:12:23 +02:00
if recipients_hidden:
2023-03-27 16:11:19 +02:00
languages.update(self._get_languages(recipients_hidden))
2021-07-12 23:12:23 +02:00
bcc = self._get_addresses(recipients_hidden)
if not bcc and self.fallback_recipients_hidden:
2023-03-27 16:11:19 +02:00
languages.update(
2021-07-12 23:12:23 +02:00
self._get_languages(self.fallback_recipients_hidden))
bcc = self._get_addresses(self.fallback_recipients_hidden)
2023-03-27 16:11:19 +02:00
return bcc, languages
2021-07-12 23:12:23 +02:00
2020-07-04 20:07:54 +02:00
@classmethod
2022-04-11 23:24:21 +02:00
def validate_fields(cls, notifications, field_names):
super().validate_fields(notifications, field_names)
cls.check_subject(notifications, field_names)
2020-07-04 20:07:54 +02:00
2022-04-11 23:24:21 +02:00
@classmethod
def check_subject(cls, notifications, field_names=None):
if field_names and 'subject' not in field_names:
2020-07-04 20:07:54 +02:00
return
2022-04-11 23:24:21 +02:00
for notification in notifications:
if not notification.subject:
continue
try:
TextTemplate(notification.subject)
except Exception as exception:
raise TemplateError(gettext(
'notification_email.'
'msg_notification_invalid_subject',
notification=notification.rec_name,
exception=exception)) from exception
2020-07-04 20:07:54 +02:00
2017-06-24 10:51:20 +02:00
2017-08-02 22:53:56 +02:00
class EmailAttachment(ModelSQL):
__name__ = 'notification.email.attachment'
notification = fields.Many2One(
'notification.email', "Notification", required=True)
2017-08-02 22:53:56 +02:00
report = fields.Many2One(
'ir.action.report', "Report", required=True,
domain=[
('model', '=', Eval('model')),
])
2017-08-02 22:53:56 +02:00
model = fields.Function(fields.Char("Model"), 'get_model')
def get_model(self, name):
return self.notification.model
@classmethod
2024-01-25 16:04:05 +01:00
def get(cls, report, record, language):
2017-08-02 22:53:56 +02:00
pool = Pool()
Report = pool.get(report.report_name, type='report')
with Transaction().set_context(language=language):
ext, content, _, title = Report.execute(
[record.id], {
'action_id': report.id,
})
2025-06-15 12:19:39 +02:00
name = '%s.%s' % (slugify(title), ext)
2024-01-25 16:04:05 +01:00
if isinstance(content, str):
content = content.encode('utf-8')
return name, content