Files
tryton/modules/notification_email/notification.py
T

406 lines
14 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
2017-12-13 18:08:13 +01:00
from email.encoders import encode_base64
2017-06-24 10:51:20 +02:00
from email.header import Header
2017-08-02 22:53:56 +02:00
from email.mime.application import MIMEApplication
2017-06-24 10:51:20 +02:00
from email.mime.multipart import MIMEMultipart
from email.mime.nonmultipart import MIMENonMultipart
from email.utils import formataddr, getaddresses, parseaddr
2017-06-24 10:51:20 +02:00
2021-08-12 18:36:32 +02:00
from sql import Null
2020-08-11 22:00:26 +02:00
from sql.operators import Concat
2020-07-04 20:07:54 +02:00
from genshi.template import TextTemplate
2017-06-24 10:51:20 +02:00
from trytond.config import config
2020-07-04 20:07:54 +02:00
from trytond.i18n import gettext
2017-06-24 10:51:20 +02:00
from trytond.model import ModelView, ModelSQL, fields
from trytond.pool import Pool
from trytond.pyson import Eval
2017-08-28 23:15:47 +02:00
from trytond.report import get_email
2017-06-24 10:51:20 +02:00
from trytond.sendmail import sendmail_transactional, SMTPDataManager
from trytond.transaction import Transaction
2020-07-04 20:07:54 +02:00
from .exceptions import TemplateError
2017-06-24 10:51:20 +02:00
2020-08-11 22:00:26 +02:00
from trytond.ir.resource import ResourceAccessMixin
2017-06-24 10:51:20 +02:00
class Email(ModelSQL, ModelView):
2020-05-08 17:45:42 +02:00
"Email Notification"
2017-06-24 10:51:20 +02:00
__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=[
('model.model', '=', Eval('model')),
],
depends=['model'],
help="The field that contains the recipient(s).")
2018-03-10 17:37:01 +01:00
fallback_recipients = fields.Many2One(
'res.user', "Recipients Fallback User",
domain=[
('email', '!=', None),
],
states={
'invisible': ~Eval('recipients'),
},
depends=['recipients'],
help="User notified when no recipients e-mail is found")
2017-06-24 10:51:20 +02:00
recipients_secondary = fields.Many2One(
'ir.model.field', "Secondary Recipients",
domain=[
('model.model', '=', Eval('model')),
],
depends=['model'],
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 User",
domain=[
('email', '!=', None),
],
states={
'invisible': ~Eval('recipients_secondary'),
},
depends=['recipients'],
help="User notified when no secondary recipients e-mail is found")
2017-06-24 10:51:20 +02:00
recipients_hidden = fields.Many2One(
'ir.model.field', "Hidden Recipients",
domain=[
('model.model', '=', Eval('model')),
],
depends=['model'],
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 User",
domain=[
('email', '!=', None),
],
states={
'invisible': ~Eval('recipients_hidden'),
},
depends=['recipients_hidden'],
help="User notified when no hidden recipients e-mail 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')),
],
depends=['model'],
help="The reports used as attachments.")
2017-06-24 10:51:20 +02:00
triggers = fields.One2Many(
'ir.trigger', 'notification_email', "Triggers",
domain=[('model.model', '=', Eval('model'))],
depends=['model'],
help="Add a trigger for the notification.")
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()),
[
('model.model', 'in', EmailTemplate.email_models()),
('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):
return 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
2017-07-12 20:02:18 +02:00
def get_email(self, record, from_, 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
content, title = get_email(self.content, record, languages)
2020-07-04 20:07:54 +02:00
language = list(languages)[-1]
with Transaction().set_context(language=language.code):
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:
msg = MIMEMultipart('mixed')
msg.attach(content)
for report in self.attachments:
msg.attach(Attachment.get_mime(report, record, language.code))
2017-08-02 22:53:56 +02:00
else:
msg = content
2017-07-12 20:02:18 +02:00
msg['From'] = from_
msg['To'] = ', '.join(formataddr(parseaddr(a)) for a in to)
msg['Cc'] = ', '.join(formataddr(parseaddr(a)) for a in cc)
2017-06-24 10:51:20 +02:00
msg['Subject'] = Header(title, 'utf-8')
2017-08-02 22:52:12 +02:00
msg['Auto-Submitted'] = 'auto-generated'
2017-06-24 10:51:20 +02:00
return msg
def get_log(self, record, trigger, msg, bcc=None):
2017-06-24 10:51:20 +02:00
return {
'recipients': msg['To'],
'recipients_secondary': msg['Cc'],
'recipients_hidden': bcc,
2020-08-11 22:00:26 +02:00
'resource': str(record),
'notification': trigger.notification_email.id,
2017-06-24 10:51:20 +02:00
'trigger': trigger.id,
}
@classmethod
def trigger(cls, records, trigger):
"Action function for the triggers"
if not trigger.notification_email:
raise ValueError(
'Trigger "%s" is not related to any email notification'
% trigger.rec_name)
trigger.notification_email.send_email(records, trigger)
def send_email(self, records, trigger):
pool = Pool()
Log = pool.get('notification.email.log')
2017-06-24 10:51:20 +02:00
datamanager = SMTPDataManager()
Transaction().join(datamanager)
from_ = (config.get('notification_email', 'from')
or config.get('email', 'from'))
2017-06-24 10:51:20 +02:00
logs = []
for record in records:
languagues = set()
to = []
if self.recipients:
recipients = getattr(record, self.recipients.name, None)
if recipients:
languagues.update(self._get_languages(recipients))
to = self._get_addresses(recipients)
2018-03-10 17:37:01 +01:00
if not to and self.fallback_recipients:
languagues.update(
self._get_languages(self.fallback_recipients))
to = self._get_addresses(self.fallback_recipients)
2017-06-24 10:51:20 +02:00
cc = []
if self.recipients_secondary:
recipients_secondary = getattr(
record, self.recipients_secondary.name, None)
if recipients_secondary:
languagues.update(
self._get_languages(recipients_secondary))
cc = self._get_addresses(recipients_secondary)
2018-03-10 17:37:01 +01:00
if not cc and self.fallback_recipients_secondary:
languagues.update(
self._get_languages(self.fallback_recipients_secondary))
cc = self._get_addresses(self.fallback_recipients_secondary)
2017-06-24 10:51:20 +02:00
bcc = []
if self.recipients_hidden:
recipients_hidden = getattr(
record, self.recipients_hidden.name, None)
if recipients_hidden:
languagues.update(self._get_languages(recipients_hidden))
bcc = self._get_addresses(recipients_hidden)
2018-03-10 17:37:01 +01:00
if not bcc and self.fallback_recipients_hidden:
languagues.update(
self._get_languages(self.fallback_recipients_hidden))
bcc = self._get_addresses(self.fallback_recipients_hidden)
2017-06-24 10:51:20 +02:00
to_addrs = [e for _, e in getaddresses(to + cc + bcc)]
if to_addrs:
2020-07-27 16:16:17 +02:00
msg = self.get_email(record, from_, to, cc, bcc, languagues)
2017-06-24 10:51:20 +02:00
sendmail_transactional(
from_, to_addrs, msg, datamanager=datamanager)
logs.append(self.get_log(
record, trigger, msg, bcc=', '.join(bcc)))
2017-06-24 10:51:20 +02:00
if logs:
Log.create(logs)
2020-07-04 20:07:54 +02:00
@classmethod
def validate(cls, notifications):
super().validate(notifications)
for notification in notifications:
notification.check_subject()
def check_subject(self):
if not self.subject:
return
try:
TextTemplate(self.subject)
except Exception as exception:
raise TemplateError(
gettext('notification_email.'
'msg_notification_invalid_subject',
notification=self.rec_name,
exception=exception)) from exception
2017-06-24 10:51:20 +02:00
2017-08-02 22:53:56 +02:00
class EmailAttachment(ModelSQL):
"Email Notification Attachment"
__name__ = 'notification.email.attachment'
notification = fields.Many2One(
'notification.email', "Notification",
required=True, select=True)
report = fields.Many2One(
'ir.action.report', "Report", required=True,
domain=[
('model', '=', Eval('model')),
],
depends=['model'])
model = fields.Function(fields.Char("Model"), 'get_model')
def get_model(self, name):
return self.notification.model
@classmethod
def get_mime(cls, report, record, language):
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,
})
name = '%s.%s' % (title, ext)
mimetype, _ = mimetypes.guess_type(name)
if mimetype:
msg = MIMENonMultipart(*mimetype.split('/'))
msg.set_payload(content)
2017-12-13 18:08:13 +01:00
encode_base64(msg)
else:
msg = MIMEApplication(content)
2017-08-02 22:53:56 +02:00
if not isinstance(name, str):
name = name.encode('utf-8')
if not isinstance(language, str):
language = language.encode('utf-8')
msg.add_header(
'Content-Disposition', 'attachment',
filename=('utf-8', language, name))
return msg
2020-08-11 22:00:26 +02:00
class EmailLog(ResourceAccessMixin, ModelSQL, ModelView):
2020-05-08 17:45:42 +02:00
"Notification Email Log"
__name__ = 'notification.email.log'
2017-06-24 10:51:20 +02:00
date = fields.Function(fields.DateTime('Date'), 'get_date')
recipients = fields.Char("Recipients")
recipients_secondary = fields.Char("Secondary Recipients")
recipients_hidden = fields.Char("Hidden Recipients")
2020-08-11 22:00:26 +02:00
notification = fields.Many2One(
'notification.email', "Notification",
required=True, ondelete='RESTRICT')
trigger = fields.Many2One('ir.trigger', "Trigger")
2017-06-24 10:51:20 +02:00
@classmethod
def __setup__(cls):
super().__setup__()
cls._order = [
('create_date', 'DESC'),
('id', 'DESC'),
]
2020-08-11 22:00:26 +02:00
@classmethod
def __register__(cls, module_name):
pool = Pool()
Model = pool.get('ir.model')
Trigger = pool.get('ir.trigger')
model = Model.__table__()
trigger = Trigger.__table__()
table = cls.__table__()
super().__register__(module_name)
table_h = cls.__table_handler__(module_name)
cursor = Transaction().connection.cursor()
# Migration from 5.6:
# fill notification and resource
# remove required on trigger
notification = trigger.select(
trigger.notification_email,
where=trigger.id == table.trigger)
resource = (trigger
.join(model, condition=trigger.model == model.id)
.select(
Concat(model.model, ',-1'),
where=trigger.id == table.trigger))
cursor.execute(*table.update(
[table.notification, table.resource],
2021-08-12 18:36:32 +02:00
[notification, resource],
2021-10-13 18:27:52 +02:00
where=(table.trigger != Null) & (table.resource == Null)))
2020-08-11 22:00:26 +02:00
table_h.not_null_action('trigger', 'remove')
2017-06-24 10:51:20 +02:00
def get_date(self, name):
return self.create_date.replace(microsecond=0)
@classmethod
def search_date(cls, name, clause):
return [('create_date',) + tuple(clause[1:])]
@staticmethod
def order_date(tables):
table, _ = tables[None]
return [table.create_date]