[MOV][REF] mail: add failure type on mail.mail model and move computation in mail
RATIONALE Currently most of mail related failure types are computed in mass mailing. This is mainly due to historical reasons. We could move some of this computation directly at mail level and store failure information directly in mail.mail records. SPECIFICATIONS Overall purpose is to get near SMS implementation where base composer prepare more advanced pre computation: blacklist, optout, seen list. Detect and store failure types directly on mail.mail: blacklist, optout, duplicates, missing email, wrong email, ... State computation is moved from mass mailing to mail. It is also improved as currently everything is based on "first recipient found". This is globally valid for mass mailing who uses default recipients (customer). However when dealing with generic mailing this is not true anymore. We choose to store and update status on mail.mail records only when having a single recipient. When several recipients are defined on a given mail.mail we cannot really decide a status prior to sending it and skip this computation. Mass mailing override now uses information from mail.mail to update its linked traces as computation is now delegated to base composer model. LINKS Task ID-2377974 Community PR odoo#61467 Enterprise PR odoo/enterprise#14633 Upgrade PR odoo/upgrade#1907
This commit is contained in:
@@ -50,12 +50,24 @@ class MailMail(models.Model):
|
||||
('exception', 'Delivery Failed'),
|
||||
('cancel', 'Cancelled'),
|
||||
], 'Status', readonly=True, copy=False, default='outgoing')
|
||||
auto_delete = fields.Boolean(
|
||||
'Auto Delete',
|
||||
help="This option permanently removes any track of email after it's been sent, including from the Technical menu in the Settings, in order to preserve storage space of your Odoo database.")
|
||||
failure_type = fields.Selection(selection=[
|
||||
# generic
|
||||
("unknown", "Unknown error"),
|
||||
# mail
|
||||
("mail_email_invalid", "Invalid email address"),
|
||||
("mail_email_missing", "Missing email"),
|
||||
("mail_smtp", "Connection failed (outgoing mail server problem)"),
|
||||
# mass mode
|
||||
("mail_bl", "Blacklisted Address"),
|
||||
("mail_optout", "Opted Out"),
|
||||
("mail_dup", "Duplicated Email"),
|
||||
], string='Failure type')
|
||||
failure_reason = fields.Text(
|
||||
'Failure Reason', readonly=1, copy=False,
|
||||
help="Failure reason. This is usually the exception thrown by the email server, stored to ease the debugging of mailing issues.")
|
||||
auto_delete = fields.Boolean(
|
||||
'Auto Delete',
|
||||
help="This option permanently removes any track of email after it's been sent, including from the Technical menu in the Settings, in order to preserve storage space of your Odoo database.")
|
||||
scheduled_date = fields.Char('Scheduled Send Date',
|
||||
help="If set, the queue manager will send the email after the date. If not set, the email will be send as soon as possible.")
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import re
|
||||
|
||||
from odoo import _, api, fields, models, tools, Command
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import email_re
|
||||
|
||||
|
||||
def _reopen(self, res_id, model, context=None):
|
||||
@@ -350,17 +351,6 @@ class MailComposer(models.TransientModel):
|
||||
records = self.env[self.model].browse(res_ids)
|
||||
reply_to_value = records._notify_get_reply_to(default=self.email_from)
|
||||
|
||||
blacklisted_rec_ids = set()
|
||||
if mass_mail_mode and issubclass(type(self.env[self.model]), self.pool['mail.thread.blacklist']):
|
||||
self.env['mail.blacklist'].flush(['email'])
|
||||
self._cr.execute("SELECT email FROM mail_blacklist WHERE active=true")
|
||||
blacklist = {x[0] for x in self._cr.fetchall()}
|
||||
if blacklist:
|
||||
targets = self.env[self.model].browse(res_ids).read(['email_normalized'])
|
||||
# First extract email from recipient before comparing with blacklist
|
||||
blacklisted_rec_ids.update(target['id'] for target in targets
|
||||
if target['email_normalized'] in blacklist)
|
||||
|
||||
for res_id in res_ids:
|
||||
# static wizard (mail.message) values
|
||||
mail_values = {
|
||||
@@ -412,15 +402,115 @@ class MailComposer(models.TransientModel):
|
||||
attachment_ids,
|
||||
{'model': 'mail.message', 'res_id': 0}
|
||||
)['attachment_ids']
|
||||
# Filter out the blacklisted records by setting the mail state to cancel -> Used for Mass Mailing stats
|
||||
if res_id in blacklisted_rec_ids:
|
||||
mail_values['state'] = 'cancel'
|
||||
# Do not post the mail into the recipient's chatter
|
||||
mail_values['notification'] = False
|
||||
|
||||
results[res_id] = mail_values
|
||||
|
||||
results = self._process_state(results)
|
||||
return results
|
||||
|
||||
def _process_recipient_values(self, mail_values_dict):
|
||||
# Preprocess res.partners to batch-fetch from db if recipient_ids is present
|
||||
# it means they are partners (the only object to fill get_default_recipient this way)
|
||||
recipient_pids = [
|
||||
recipient_command[1]
|
||||
for mail_values in mail_values_dict.values()
|
||||
# recipient_ids is a list of x2m command tuples at this point
|
||||
for recipient_command in mail_values.get('recipient_ids') or []
|
||||
if recipient_command[1]
|
||||
]
|
||||
recipient_emails = {
|
||||
p.id: p.email
|
||||
for p in self.env['res.partner'].browse(set(recipient_pids))
|
||||
} if recipient_pids else {}
|
||||
|
||||
recipients_info = {}
|
||||
for record_id, mail_values in mail_values_dict.items():
|
||||
mail_to = []
|
||||
if mail_values.get('email_to'):
|
||||
mail_to += email_re.findall(mail_values['email_to'])
|
||||
# if unrecognized email in email_to -> keep it as used for further processing
|
||||
if not mail_to:
|
||||
mail_to.append(mail_values['email_to'])
|
||||
# add email from recipients (res.partner)
|
||||
mail_to += [
|
||||
recipient_emails[recipient_command[1]]
|
||||
for recipient_command in mail_values.get('recipient_ids') or []
|
||||
if recipient_command[1]
|
||||
]
|
||||
mail_to = list(set(mail_to))
|
||||
recipients_info[record_id] = {
|
||||
'mail_to': mail_to,
|
||||
'mail_to_normalized': [
|
||||
tools.email_normalize(mail)
|
||||
for mail in mail_to
|
||||
if tools.email_normalize(mail)
|
||||
]
|
||||
}
|
||||
return recipients_info
|
||||
|
||||
def _process_state(self, mail_values_dict):
|
||||
recipients_info = self._process_recipient_values(mail_values_dict)
|
||||
blacklist_ids = self._get_blacklist_record_ids(mail_values_dict)
|
||||
optout_emails = self._get_optout_emails(mail_values_dict)
|
||||
done_emails = self._get_done_emails(mail_values_dict)
|
||||
|
||||
for record_id, mail_values in mail_values_dict.items():
|
||||
recipients = recipients_info[record_id]
|
||||
# when having more than 1 recipient: we cannot really decide when a single
|
||||
# email is linked to several to -> skip that part. Mass mailing should
|
||||
# anyway always have a single recipient per record as this is default behavior.
|
||||
if len(recipients['mail_to']) > 1:
|
||||
continue
|
||||
|
||||
mail_to = recipients['mail_to'][0] if recipients['mail_to'] else ''
|
||||
mail_to_normalized = recipients['mail_to_normalized'][0] if recipients['mail_to_normalized'] else ''
|
||||
|
||||
# prevent sending to blocked addresses that were included by mistake
|
||||
# blacklisted or optout or duplicate -> cancel
|
||||
if record_id in blacklist_ids:
|
||||
mail_values['state'] = 'cancel'
|
||||
mail_values['failure_type'] = 'mail_bl'
|
||||
# Do not post the mail into the recipient's chatter
|
||||
mail_values['notification'] = False
|
||||
elif optout_emails and mail_to in optout_emails:
|
||||
mail_values['state'] = 'cancel'
|
||||
mail_values['failure_type'] = 'mail_optout'
|
||||
elif done_emails and mail_to in done_emails:
|
||||
mail_values['state'] = 'cancel'
|
||||
mail_values['failure_type'] = 'mail_dup'
|
||||
# void of falsy values -> error
|
||||
elif not mail_to:
|
||||
mail_values['state'] = 'cancel'
|
||||
mail_values['failure_type'] = 'mail_email_missing'
|
||||
elif not mail_to_normalized or not email_re.findall(mail_to):
|
||||
mail_values['state'] = 'cancel'
|
||||
mail_values['failure_type'] = 'mail_email_invalid'
|
||||
elif done_emails is not None:
|
||||
done_emails.append(mail_to)
|
||||
|
||||
return mail_values_dict
|
||||
|
||||
def _get_blacklist_record_ids(self, mail_values_dict):
|
||||
blacklisted_rec_ids = set()
|
||||
if self.composition_mode == 'mass_mail' and issubclass(type(self.env[self.model]), self.pool['mail.thread.blacklist']):
|
||||
self.env['mail.blacklist'].flush(['email'])
|
||||
self._cr.execute("SELECT email FROM mail_blacklist WHERE active=true")
|
||||
blacklist = {x[0] for x in self._cr.fetchall()}
|
||||
if blacklist:
|
||||
targets = self.env[self.model].browse(mail_values_dict.keys()).read(['email_normalized'])
|
||||
# First extract email from recipient before comparing with blacklist
|
||||
blacklisted_rec_ids.update(target['id'] for target in targets
|
||||
if target['email_normalized'] in blacklist)
|
||||
return blacklisted_rec_ids
|
||||
|
||||
def _get_done_emails(self, mail_values_dict):
|
||||
seen_list = self._context.get('mass_mailing_seen_list') or []
|
||||
return seen_list
|
||||
|
||||
def _get_optout_emails(self, mail_values_dict):
|
||||
opt_out_list = self._context.get('mass_mailing_opt_out_list') or []
|
||||
return opt_out_list
|
||||
|
||||
def _onchange_template_id(self, template_id, composition_mode, model, res_id):
|
||||
""" - mass_mailing: we cannot render, so return the template values
|
||||
- normal mode: return rendered values
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, tools
|
||||
from odoo.tools import email_re
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class MailComposeMessage(models.TransientModel):
|
||||
@@ -40,70 +39,27 @@ class MailComposeMessage(models.TransientModel):
|
||||
'attachment_ids': [(6, 0, self.attachment_ids.ids)],
|
||||
})
|
||||
|
||||
# Preprocess res.partners to batch-fetch from db
|
||||
# if recipient_ids is present, it means they are partners
|
||||
# (the only object to fill get_default_recipient this way)
|
||||
recipient_partners_ids = []
|
||||
read_partners = {}
|
||||
for res_id in res_ids:
|
||||
mail_values = res[res_id]
|
||||
if mail_values.get('recipient_ids'):
|
||||
# recipient_ids is a list of x2m command tuples at this point
|
||||
recipient_partners_ids.append(mail_values.get('recipient_ids')[0][1])
|
||||
read_partners = self.env['res.partner'].browse(recipient_partners_ids)
|
||||
|
||||
partners_email = {p.id: p.email for p in read_partners}
|
||||
|
||||
opt_out_list = self._context.get('mass_mailing_opt_out_list')
|
||||
seen_list = self._context.get('mass_mailing_seen_list')
|
||||
recipients_info = self._process_recipient_values(res)
|
||||
mass_mail_layout = self.env.ref('mass_mailing.mass_mailing_mail_layout', raise_if_not_found=False)
|
||||
for res_id in res_ids:
|
||||
mail_values = res[res_id]
|
||||
if mail_values.get('body_html') and mass_mail_layout:
|
||||
mail_values['body_html'] = mass_mail_layout._render({'body': mail_values['body_html']}, engine='ir.qweb', minimal_qcontext=True)
|
||||
|
||||
mail_values = res[res_id]
|
||||
if mail_values.get('email_to'):
|
||||
mail_to = mail_values['email_to']
|
||||
else:
|
||||
partner_id = (mail_values.get('recipient_ids') or [(False, '')])[0][1]
|
||||
mail_to = partners_email.get(partner_id)
|
||||
mail_to_normalized = tools.email_normalize(mail_to)
|
||||
|
||||
# prevent sending to blocked addresses that were included by mistake
|
||||
# blacklisted or optout or duplicate -> cancel
|
||||
failure_type = False
|
||||
if mail_values.get('state') == 'cancel':
|
||||
failure_type = 'mail_bl'
|
||||
elif opt_out_list and mail_to in opt_out_list:
|
||||
mail_values['state'] = 'cancel'
|
||||
failure_type = 'mail_optout'
|
||||
elif seen_list and mail_to in seen_list:
|
||||
mail_values['state'] = 'cancel'
|
||||
failure_type = 'mail_dup'
|
||||
# void of falsey values -> error
|
||||
elif not mail_to:
|
||||
mail_values['state'] = 'cancel'
|
||||
failure_type = 'mail_email_missing'
|
||||
elif not mail_to_normalized or not email_re.findall(mail_to):
|
||||
mail_values['state'] = 'cancel'
|
||||
failure_type = "mail_email_invalid"
|
||||
elif seen_list is not None:
|
||||
seen_list.add(mail_to)
|
||||
|
||||
trace_vals = {
|
||||
'model': self.model,
|
||||
'res_id': res_id,
|
||||
'mass_mailing_id': mass_mailing.id,
|
||||
# if mail_to is void, keep falsy values to allow searching / debugging traces
|
||||
'email': mail_to or mail_values.get('email_to'),
|
||||
'email': recipients_info[res_id]['mail_to'][0] if recipients_info[res_id]['mail_to'] else '',
|
||||
}
|
||||
# propagate failed states to trace when still-born
|
||||
if mail_values.get('state') == 'cancel':
|
||||
trace_vals['trace_status'] = 'cancel'
|
||||
elif mail_values.get('state') == 'exception':
|
||||
trace_vals['trace_status'] = 'error'
|
||||
if failure_type:
|
||||
trace_vals['failure_type'] = failure_type
|
||||
if mail_values.get('failure_type'):
|
||||
trace_vals['failure_type'] = mail_values['failure_type']
|
||||
|
||||
mail_values.update({
|
||||
'mailing_id': mass_mailing.id,
|
||||
|
||||
Reference in New Issue
Block a user