[IMP] mail_tracking: black, isort

This commit is contained in:
Alexandre Díaz 2019-11-18 11:46:59 +01:00 committed by Jasmin Solanki
parent 9d4c4d3533
commit 200e016ab8
13 changed files with 894 additions and 787 deletions

View File

@ -10,15 +10,11 @@
"version": "12.0.2.0.1", "version": "12.0.2.0.1",
"category": "Social Network", "category": "Social Network",
"website": "http://github.com/OCA/social", "website": "http://github.com/OCA/social",
"author": "Tecnativa, " "author": ("Tecnativa, " "Odoo Community Association (OCA)"),
"Odoo Community Association (OCA)",
"license": "AGPL-3", "license": "AGPL-3",
"application": False, "application": False,
'installable': True, "installable": True,
"depends": [ "depends": ["decimal_precision", "mail"],
"decimal_precision",
"mail",
],
"data": [ "data": [
"data/tracking_data.xml", "data/tracking_data.xml",
"security/mail_tracking_email_security.xml", "security/mail_tracking_email_security.xml",
@ -35,8 +31,6 @@
"static/src/xml/failed_message/thread.xml", "static/src/xml/failed_message/thread.xml",
"static/src/xml/failed_message/discuss.xml", "static/src/xml/failed_message/discuss.xml",
], ],
'demo': [ "demo": ["demo/demo.xml"],
'demo/demo.xml',
],
"pre_init_hook": "pre_init_hook", "pre_init_hook": "pre_init_hook",
} }

View File

@ -1,17 +1,20 @@
# Copyright 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com> # Copyright 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import werkzeug import base64
import odoo import logging
from contextlib import contextmanager from contextlib import contextmanager
from odoo import api, http, SUPERUSER_ID
import werkzeug
import odoo
from odoo import SUPERUSER_ID, api, http
from odoo.addons.mail.controllers.main import MailController from odoo.addons.mail.controllers.main import MailController
import logging
import base64
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
BLANK = 'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' BLANK = "R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
@contextmanager @contextmanager
@ -28,60 +31,74 @@ def db_env(dbname):
class MailTrackingController(MailController): class MailTrackingController(MailController):
def _request_metadata(self): def _request_metadata(self):
"""Prepare remote info metadata""" """Prepare remote info metadata"""
request = http.request.httprequest request = http.request.httprequest
return { return {
'ip': request.remote_addr or False, "ip": request.remote_addr or False,
'user_agent': request.user_agent or False, "user_agent": request.user_agent or False,
'os_family': request.user_agent.platform or False, "os_family": request.user_agent.platform or False,
'ua_family': request.user_agent.browser or False, "ua_family": request.user_agent.browser or False,
} }
@http.route(['/mail/tracking/all/<string:db>', @http.route(
'/mail/tracking/event/<string:db>/<string:event_type>'], [
type='http', auth='none', csrf=False) "/mail/tracking/all/<string:db>",
"/mail/tracking/event/<string:db>/<string:event_type>",
],
type="http",
auth="none",
csrf=False,
)
def mail_tracking_event(self, db, event_type=None, **kw): def mail_tracking_event(self, db, event_type=None, **kw):
"""Route used by external mail service""" """Route used by external mail service"""
metadata = self._request_metadata() metadata = self._request_metadata()
res = None res = None
with db_env(db) as env: with db_env(db) as env:
try: try:
res = env['mail.tracking.email'].event_process( res = env["mail.tracking.email"].event_process(
http.request, kw, metadata, event_type=event_type) http.request, kw, metadata, event_type=event_type
)
except Exception: except Exception:
pass pass
if not res or res == 'NOT FOUND': if not res or res == "NOT FOUND":
return werkzeug.exceptions.NotAcceptable() return werkzeug.exceptions.NotAcceptable()
return res return res
@http.route(['/mail/tracking/open/<string:db>' @http.route(
'/<int:tracking_email_id>/blank.gif', [
'/mail/tracking/open/<string:db>' "/mail/tracking/open/<string:db>" "/<int:tracking_email_id>/blank.gif",
'/<int:tracking_email_id>/<string:token>/blank.gif'], "/mail/tracking/open/<string:db>"
type='http', auth='none', methods=['GET']) "/<int:tracking_email_id>/<string:token>/blank.gif",
],
type="http",
auth="none",
methods=["GET"],
)
def mail_tracking_open(self, db, tracking_email_id, token=False, **kw): def mail_tracking_open(self, db, tracking_email_id, token=False, **kw):
"""Route used to track mail openned (With & Without Token)""" """Route used to track mail openned (With & Without Token)"""
metadata = self._request_metadata() metadata = self._request_metadata()
with db_env(db) as env: with db_env(db) as env:
try: try:
tracking_email = env['mail.tracking.email'].search([ tracking_email = env["mail.tracking.email"].search(
('id', '=', tracking_email_id), [
('state', 'in', ['sent', 'delivered']), ("id", "=", tracking_email_id),
('token', '=', token), ("state", "in", ["sent", "delivered"]),
]) ("token", "=", token),
]
)
if tracking_email: if tracking_email:
tracking_email.event_create('open', metadata) tracking_email.event_create("open", metadata)
else: else:
_logger.warning( _logger.warning(
"MailTracking email '%s' not found", tracking_email_id) "MailTracking email '%s' not found", tracking_email_id
)
except Exception: except Exception:
pass pass
# Always return GIF blank image # Always return GIF blank image
response = werkzeug.wrappers.Response() response = werkzeug.wrappers.Response()
response.mimetype = 'image/gif' response.mimetype = "image/gif"
response.data = base64.b64decode(BLANK) response.data = base64.b64decode(BLANK)
return response return response
@ -89,8 +106,7 @@ class MailTrackingController(MailController):
def mail_init_messaging(self): def mail_init_messaging(self):
"""Route used to initial values of Discuss app""" """Route used to initial values of Discuss app"""
values = super().mail_init_messaging() values = super().mail_init_messaging()
values.update({ values.update(
'failed_counter': {"failed_counter": http.request.env["mail.message"].get_failed_count()}
http.request.env['mail.message'].get_failed_count(), )
})
return values return values

View File

@ -2,34 +2,40 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging import logging
from psycopg2.extensions import AsIs from psycopg2.extensions import AsIs
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
def column_exists(cr, table, column): def column_exists(cr, table, column):
cr.execute(""" cr.execute(
"""
SELECT column_name SELECT column_name
FROM information_schema.columns FROM information_schema.columns
WHERE table_name = %s AND column_name = %s""", (table, column)) WHERE table_name = %s AND column_name = %s""",
(table, column),
)
return bool(cr.fetchall()) return bool(cr.fetchall())
def column_add_with_value(cr, table, column, field_type, value): def column_add_with_value(cr, table, column, field_type, value):
if not column_exists(cr, table, column): if not column_exists(cr, table, column):
cr.execute(""" cr.execute(
"""
ALTER TABLE %s ALTER TABLE %s
ADD COLUMN %s %s""", (AsIs(table), AsIs(column), AsIs(field_type))) ADD COLUMN %s %s""",
cr.execute(""" (AsIs(table), AsIs(column), AsIs(field_type)),
UPDATE %s SET %s = %s""", (AsIs(table), AsIs(column), value)) )
cr.execute(
"""
UPDATE %s SET %s = %s""",
(AsIs(table), AsIs(column), value),
)
def pre_init_hook(cr): def pre_init_hook(cr):
_logger.info("Creating res.partner.tracking_emails_count column " _logger.info("Creating res.partner.tracking_emails_count column " "with value 0")
"with value 0") column_add_with_value(cr, "res_partner", "tracking_emails_count", "integer", 0)
column_add_with_value( _logger.info("Creating res.partner.email_score column " "with value 50.0")
cr, "res_partner", "tracking_emails_count", "integer", 0) column_add_with_value(cr, "res_partner", "email_score", "double precision", 50.0)
_logger.info("Creating res.partner.email_score column "
"with value 50.0")
column_add_with_value(
cr, "res_partner", "email_score", "double precision", 50.0)

View File

@ -3,7 +3,8 @@
import re import re
import threading import threading
from odoo import models, api, tools
from odoo import api, models, tools
class IrMailServer(models.Model): class IrMailServer(models.Model):
@ -12,40 +13,63 @@ class IrMailServer(models.Model):
def _tracking_headers_add(self, tracking_email_id, headers): def _tracking_headers_add(self, tracking_email_id, headers):
"""Allow other addons to add its own tracking SMTP headers""" """Allow other addons to add its own tracking SMTP headers"""
headers = headers or {} headers = headers or {}
headers['X-Odoo-Database'] = getattr( headers["X-Odoo-Database"] = (
threading.currentThread(), 'dbname', None), getattr(threading.currentThread(), "dbname", None),
headers['X-Odoo-Tracking-ID'] = tracking_email_id )
headers["X-Odoo-Tracking-ID"] = tracking_email_id
return headers return headers
def _tracking_email_id_body_get(self, body): def _tracking_email_id_body_get(self, body):
body = body or '' body = body or ""
# https://regex101.com/r/lW4cB1/2 # https://regex101.com/r/lW4cB1/2
match = re.search( match = re.search(r'<img[^>]*data-odoo-tracking-email=["\']([0-9]*)["\']', body)
r'<img[^>]*data-odoo-tracking-email=["\']([0-9]*)["\']', body)
return int(match.group(1)) if match.group(1) else False return int(match.group(1)) if match.group(1) else False
def build_email(self, email_from, email_to, subject, body, email_cc=None, def build_email(
email_bcc=None, reply_to=False, attachments=None, self,
message_id=None, references=None, object_id=False, email_from,
subtype='plain', headers=None, body_alternative=None, email_to,
subtype_alternative='plain'): subject,
body,
email_cc=None,
email_bcc=None,
reply_to=False,
attachments=None,
message_id=None,
references=None,
object_id=False,
subtype="plain",
headers=None,
body_alternative=None,
subtype_alternative="plain",
):
tracking_email_id = self._tracking_email_id_body_get(body) tracking_email_id = self._tracking_email_id_body_get(body)
if tracking_email_id: if tracking_email_id:
headers = self._tracking_headers_add(tracking_email_id, headers) headers = self._tracking_headers_add(tracking_email_id, headers)
msg = super(IrMailServer, self).build_email( msg = super(IrMailServer, self).build_email(
email_from, email_to, subject, body, email_cc=email_cc, email_from,
email_bcc=email_bcc, reply_to=reply_to, attachments=attachments, email_to,
message_id=message_id, references=references, object_id=object_id, subject,
subtype=subtype, headers=headers, body,
email_cc=email_cc,
email_bcc=email_bcc,
reply_to=reply_to,
attachments=attachments,
message_id=message_id,
references=references,
object_id=object_id,
subtype=subtype,
headers=headers,
body_alternative=body_alternative, body_alternative=body_alternative,
subtype_alternative=subtype_alternative) subtype_alternative=subtype_alternative,
)
return msg return msg
def _tracking_email_get(self, message): def _tracking_email_get(self, message):
tracking_email_id = False tracking_email_id = False
if message.get('X-Odoo-Tracking-ID', '').isdigit(): if message.get("X-Odoo-Tracking-ID", "").isdigit():
tracking_email_id = int(message['X-Odoo-Tracking-ID']) tracking_email_id = int(message["X-Odoo-Tracking-ID"])
return self.env['mail.tracking.email'].browse(tracking_email_id) return self.env["mail.tracking.email"].browse(tracking_email_id)
def _smtp_server_get(self, mail_server_id, smtp_server): def _smtp_server_get(self, mail_server_id, smtp_server):
smtp_server_used = False smtp_server_used = False
@ -53,36 +77,49 @@ class IrMailServer(models.Model):
if mail_server_id: if mail_server_id:
mail_server = self.browse(mail_server_id) mail_server = self.browse(mail_server_id)
elif not smtp_server: elif not smtp_server:
mail_server_ids = self.search([], order='sequence', limit=1) mail_server_ids = self.search([], order="sequence", limit=1)
mail_server = mail_server_ids[0] if mail_server_ids else None mail_server = mail_server_ids[0] if mail_server_ids else None
if mail_server: if mail_server:
smtp_server_used = mail_server.smtp_host smtp_server_used = mail_server.smtp_host
else: else:
smtp_server_used = smtp_server or tools.config.get('smtp_server') smtp_server_used = smtp_server or tools.config.get("smtp_server")
return smtp_server_used return smtp_server_used
@api.model @api.model
def send_email(self, message, mail_server_id=None, smtp_server=None, def send_email(
smtp_port=None, smtp_user=None, smtp_password=None, self,
smtp_encryption=None, smtp_debug=False, smtp_session=None): message,
mail_server_id=None,
smtp_server=None,
smtp_port=None,
smtp_user=None,
smtp_password=None,
smtp_encryption=None,
smtp_debug=False,
smtp_session=None,
):
message_id = False message_id = False
tracking_email = self._tracking_email_get(message) tracking_email = self._tracking_email_get(message)
smtp_server_used = self.sudo()._smtp_server_get( smtp_server_used = self.sudo()._smtp_server_get(mail_server_id, smtp_server)
mail_server_id, smtp_server,
)
try: try:
message_id = super(IrMailServer, self).send_email( message_id = super(IrMailServer, self).send_email(
message, mail_server_id=mail_server_id, message,
smtp_server=smtp_server, smtp_port=smtp_port, mail_server_id=mail_server_id,
smtp_user=smtp_user, smtp_password=smtp_password, smtp_server=smtp_server,
smtp_encryption=smtp_encryption, smtp_debug=smtp_debug, smtp_port=smtp_port,
smtp_session=smtp_session) smtp_user=smtp_user,
smtp_password=smtp_password,
smtp_encryption=smtp_encryption,
smtp_debug=smtp_debug,
smtp_session=smtp_session,
)
except Exception as e: except Exception as e:
if tracking_email: if tracking_email:
tracking_email.smtp_error(self, smtp_server_used, e) tracking_email.smtp_error(self, smtp_server_used, e)
if message_id and tracking_email: if message_id and tracking_email:
vals = tracking_email._tracking_sent_prepare( vals = tracking_email._tracking_sent_prepare(
self, smtp_server_used, message, message_id) self, smtp_server_used, message, message_id
)
if vals: if vals:
self.env['mail.tracking.event'].sudo().create(vals) self.env["mail.tracking.event"].sudo().create(vals)
return message_id return message_id

View File

@ -1,7 +1,7 @@
# Copyright 2018 Tecnativa - Ernesto Tejeda # Copyright 2018 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models from odoo import fields, models
class MailBouncedMixin(models.AbstractModel): class MailBouncedMixin(models.AbstractModel):
@ -10,9 +10,9 @@ class MailBouncedMixin(models.AbstractModel):
the mixin and must contain the email field of the model. the mixin and must contain the email field of the model.
""" """
_name = 'mail.bounced.mixin' _name = "mail.bounced.mixin"
_description = 'Mail bounced mixin' _description = "Mail bounced mixin"
_primary_email = ['email'] _primary_email = ["email"]
email_bounced = fields.Boolean(index=True) email_bounced = fields.Boolean(index=True)
@ -20,25 +20,25 @@ class MailBouncedMixin(models.AbstractModel):
def email_bounced_set(self, tracking_emails, reason, event=None): def email_bounced_set(self, tracking_emails, reason, event=None):
"""Inherit this method to make any other actions to the model that """Inherit this method to make any other actions to the model that
inherit the mixin""" inherit the mixin"""
if self.env.context.get('write_loop'): if self.env.context.get("write_loop"):
# We avoid with the context an infinite recursion calling write # We avoid with the context an infinite recursion calling write
# method from other write method. # method from other write method.
return True return True
partners = self.filtered(lambda r: not r.email_bounced) partners = self.filtered(lambda r: not r.email_bounced)
return partners.write({'email_bounced': True}) return partners.write({"email_bounced": True})
def write(self, vals): def write(self, vals):
[email_field] = self._primary_email [email_field] = self._primary_email
if email_field not in vals: if email_field not in vals:
return super().write(vals) return super().write(vals)
email = vals[email_field].lower() if vals[email_field] else False email = vals[email_field].lower() if vals[email_field] else False
mte_obj = self.env['mail.tracking.email'] mte_obj = self.env["mail.tracking.email"]
vals['email_bounced'] = mte_obj.email_is_bounced(email) vals["email_bounced"] = mte_obj.email_is_bounced(email)
if vals['email_bounced']: if vals["email_bounced"]:
res = mte_obj._email_last_tracking_state(email) res = mte_obj._email_last_tracking_state(email)
tracking = mte_obj.browse(res[0].get('id')) tracking = mte_obj.browse(res[0].get("id"))
event = tracking.tracking_event_ids[:1] event = tracking.tracking_event_ids[:1]
self.with_context( self.with_context(write_loop=True).email_bounced_set(
write_loop=True, tracking, event.error_details, event
).email_bounced_set(tracking, event.error_details, event) )
return super().write(vals) return super().write(vals)

View File

@ -5,27 +5,27 @@ import time
from datetime import datetime from datetime import datetime
from email.utils import COMMASPACE from email.utils import COMMASPACE
from odoo import models, fields from odoo import fields, models
class MailMail(models.Model): class MailMail(models.Model):
_inherit = 'mail.mail' _inherit = "mail.mail"
def _tracking_email_prepare(self, partner, email): def _tracking_email_prepare(self, partner, email):
"""Prepare email.tracking.email record values""" """Prepare email.tracking.email record values"""
ts = time.time() ts = time.time()
dt = datetime.utcfromtimestamp(ts) dt = datetime.utcfromtimestamp(ts)
email_to_list = email.get('email_to', []) email_to_list = email.get("email_to", [])
email_to = COMMASPACE.join(email_to_list) email_to = COMMASPACE.join(email_to_list)
return { return {
'name': self.subject, "name": self.subject,
'timestamp': '%.6f' % ts, "timestamp": "%.6f" % ts,
'time': fields.Datetime.to_string(dt), "time": fields.Datetime.to_string(dt),
'mail_id': self.id, "mail_id": self.id,
'mail_message_id': self.mail_message_id.id, "mail_message_id": self.mail_message_id.id,
'partner_id': partner.id if partner else False, "partner_id": partner.id if partner else False,
'recipient': email_to, "recipient": email_to,
'sender': self.email_from, "sender": self.email_from,
} }
def _send_prepare_values(self, partner=None): def _send_prepare_values(self, partner=None):
@ -33,5 +33,5 @@ class MailMail(models.Model):
to the email""" to the email"""
email = super()._send_prepare_values(partner=partner) email = super()._send_prepare_values(partner=partner)
vals = self._tracking_email_prepare(partner, email) vals = self._tracking_email_prepare(partner, email)
tracking_email = self.env['mail.tracking.email'].sudo().create(vals) tracking_email = self.env["mail.tracking.email"].sudo().create(vals)
return tracking_email.tracking_img_add(email) return tracking_email.tracking_img_add(email)

View File

@ -2,7 +2,7 @@
# Copyright 2019 Alexandre Díaz # Copyright 2019 Alexandre Díaz
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import _, models, api, fields from odoo import _, api, fields, models
from odoo.tools import email_split from odoo.tools import email_split
@ -10,16 +10,16 @@ class MailMessage(models.Model):
_inherit = "mail.message" _inherit = "mail.message"
# Recipients # Recipients
email_cc = fields.Char("Cc", help='Additional recipients that receive a ' email_cc = fields.Char(
'"Carbon Copy" of the e-mail') "Cc", help="Additional recipients that receive a " '"Carbon Copy" of the e-mail'
)
mail_tracking_ids = fields.One2many( mail_tracking_ids = fields.One2many(
comodel_name='mail.tracking.email', comodel_name="mail.tracking.email",
inverse_name='mail_message_id', inverse_name="mail_message_id",
string="Mail Trackings", string="Mail Trackings",
) )
mail_tracking_needs_action = fields.Boolean( mail_tracking_needs_action = fields.Boolean(
help="The message tracking will be considered" help="The message tracking will be considered" " to filter tracking issues",
" to filter tracking issues",
default=False, default=False,
) )
is_failed_message = fields.Boolean(compute="_compute_is_failed_message") is_failed_message = fields.Boolean(compute="_compute_is_failed_message")
@ -27,95 +27,105 @@ class MailMessage(models.Model):
@api.model @api.model
def get_failed_states(self): def get_failed_states(self):
"""The 'failed' states of the message""" """The 'failed' states of the message"""
return {'error', 'rejected', 'spam', 'bounced', 'soft-bounced'} return {"error", "rejected", "spam", "bounced", "soft-bounced"}
@api.depends('mail_tracking_needs_action', 'author_id', 'partner_ids', @api.depends(
'mail_tracking_ids.state') "mail_tracking_needs_action",
"author_id",
"partner_ids",
"mail_tracking_ids.state",
)
def _compute_is_failed_message(self): def _compute_is_failed_message(self):
"""Compute 'is_failed_message' field for the active user""" """Compute 'is_failed_message' field for the active user"""
failed_states = self.get_failed_states() failed_states = self.get_failed_states()
for message in self: for message in self:
needs_action = message.mail_tracking_needs_action needs_action = message.mail_tracking_needs_action
involves_me = self.env.user.partner_id in ( involves_me = self.env.user.partner_id in (
message.author_id | message.partner_ids) message.author_id | message.partner_ids
)
has_failed_trackings = failed_states.intersection( has_failed_trackings = failed_states.intersection(
message.mapped("mail_tracking_ids.state")) message.mapped("mail_tracking_ids.state")
)
message.is_failed_message = bool( message.is_failed_message = bool(
needs_action and involves_me and has_failed_trackings) needs_action and involves_me and has_failed_trackings
)
def _tracking_status_map_get(self): def _tracking_status_map_get(self):
"""Map tracking states to be used in chatter""" """Map tracking states to be used in chatter"""
return { return {
'False': 'waiting', "False": "waiting",
'error': 'error', "error": "error",
'deferred': 'sent', "deferred": "sent",
'sent': 'sent', "sent": "sent",
'delivered': 'delivered', "delivered": "delivered",
'opened': 'opened', "opened": "opened",
'rejected': 'error', "rejected": "error",
'spam': 'error', "spam": "error",
'unsub': 'opened', "unsub": "opened",
'bounced': 'error', "bounced": "error",
'soft-bounced': 'error', "soft-bounced": "error",
} }
def _partner_tracking_status_get(self, tracking_email): def _partner_tracking_status_get(self, tracking_email):
"""Determine tracking status""" """Determine tracking status"""
tracking_status_map = self._tracking_status_map_get() tracking_status_map = self._tracking_status_map_get()
status = 'unknown' status = "unknown"
if tracking_email: if tracking_email:
tracking_email_status = str(tracking_email.state) tracking_email_status = str(tracking_email.state)
status = tracking_status_map.get(tracking_email_status, 'unknown') status = tracking_status_map.get(tracking_email_status, "unknown")
return status return status
def _partner_tracking_status_human_get(self, status): def _partner_tracking_status_human_get(self, status):
"""Translations for tracking statuses to be used on qweb""" """Translations for tracking statuses to be used on qweb"""
statuses = {'waiting': _('Waiting'), 'error': _('Error'), statuses = {
'sent': _('Sent'), 'delivered': _('Delivered'), "waiting": _("Waiting"),
'opened': _('Opened'), 'unknown': _('Unknown')} "error": _("Error"),
"sent": _("Sent"),
"delivered": _("Delivered"),
"opened": _("Opened"),
"unknown": _("Unknown"),
}
return _("Status: %s") % statuses[status] return _("Status: %s") % statuses[status]
@api.model @api.model
def _get_error_description(self, tracking): def _get_error_description(self, tracking):
"""Translations for error descriptions to be used on qweb""" """Translations for error descriptions to be used on qweb"""
descriptions = { descriptions = {"no_recipient": _("The partner doesn't have a defined email")}
'no_recipient': _("The partner doesn't have a defined email"), return descriptions.get(tracking.error_type, tracking.error_description)
}
return descriptions.get(tracking.error_type,
tracking.error_description)
def tracking_status(self): def tracking_status(self):
"""Generates a complete status tracking of the messages by partner""" """Generates a complete status tracking of the messages by partner"""
res = {} res = {}
for message in self: for message in self:
partner_trackings = [] partner_trackings = []
partners_already = self.env['res.partner'] partners_already = self.env["res.partner"]
partners = self.env['res.partner'] partners = self.env["res.partner"]
trackings = self.env['mail.tracking.email'].sudo().search([ trackings = (
('mail_message_id', '=', message.id), self.env["mail.tracking.email"]
]) .sudo()
.search([("mail_message_id", "=", message.id)])
)
# Get Cc recipients # Get Cc recipients
email_cc_list = email_split(message.email_cc) email_cc_list = email_split(message.email_cc)
if any(email_cc_list): if any(email_cc_list):
partners |= partners.search([('email', 'in', email_cc_list)]) partners |= partners.search([("email", "in", email_cc_list)])
email_cc_list = set(email_cc_list) email_cc_list = set(email_cc_list)
# Search all trackings for this message # Search all trackings for this message
for tracking in trackings: for tracking in trackings:
status = self._partner_tracking_status_get(tracking) status = self._partner_tracking_status_get(tracking)
recipient = ( recipient = tracking.partner_id.name or tracking.recipient
tracking.partner_id.name or tracking.recipient) partner_trackings.append(
partner_trackings.append({ {
'status': status, "status": status,
'status_human': "status_human": self._partner_tracking_status_human_get(status),
self._partner_tracking_status_human_get(status), "error_type": tracking.error_type,
'error_type': tracking.error_type, "error_description": self._get_error_description(tracking),
'error_description': "tracking_id": tracking.id,
self._get_error_description(tracking), "recipient": recipient,
'tracking_id': tracking.id, "partner_id": tracking.partner_id.id,
'recipient': recipient, "isCc": False,
'partner_id': tracking.partner_id.id, }
'isCc': False, )
})
if tracking.partner_id: if tracking.partner_id:
email_cc_list.discard(tracking.partner_id.email) email_cc_list.discard(tracking.partner_id.email)
partners_already |= tracking.partner_id partners_already |= tracking.partner_id
@ -127,12 +137,11 @@ class MailMessage(models.Model):
# Remove recipients already included # Remove recipients already included
partners -= partners_already partners -= partners_already
tracking_unkown_values = { tracking_unkown_values = {
'status': 'unknown', "status": "unknown",
'status_human': self._partner_tracking_status_human_get( "status_human": self._partner_tracking_status_human_get("unknown"),
'unknown'), "error_type": False,
'error_type': False, "error_description": False,
'error_description': False, "tracking_id": False,
'tracking_id': False,
} }
for partner in partners: for partner in partners:
# If there is partners not included, then status is 'unknown' # If there is partners not included, then status is 'unknown'
@ -141,36 +150,31 @@ class MailMessage(models.Model):
if partner.email in email_cc_list: if partner.email in email_cc_list:
email_cc_list.discard(partner.email) email_cc_list.discard(partner.email)
isCc = True isCc = True
tracking_unkown_values.update({ tracking_unkown_values.update(
'recipient': partner.name, {"recipient": partner.name, "partner_id": partner.id, "isCc": isCc}
'partner_id': partner.id, )
'isCc': isCc,
})
partner_trackings.append(tracking_unkown_values.copy()) partner_trackings.append(tracking_unkown_values.copy())
for email in email_cc_list: for email in email_cc_list:
# If there is Cc without partner # If there is Cc without partner
tracking_unkown_values.update({ tracking_unkown_values.update(
'recipient': email, {"recipient": email, "partner_id": False, "isCc": True}
'partner_id': False, )
'isCc': True,
})
partner_trackings.append(tracking_unkown_values.copy()) partner_trackings.append(tracking_unkown_values.copy())
res[message.id] = { res[message.id] = {
'partner_trackings': partner_trackings, "partner_trackings": partner_trackings,
'is_failed_message': message.is_failed_message, "is_failed_message": message.is_failed_message,
} }
return res return res
@api.model @api.model
def _message_read_dict_postprocess(self, messages, message_tree): def _message_read_dict_postprocess(self, messages, message_tree):
"""Preare values to be used by the chatter widget""" """Preare values to be used by the chatter widget"""
res = super()._message_read_dict_postprocess( res = super()._message_read_dict_postprocess(messages, message_tree)
messages, message_tree) mail_message_ids = {m.get("id") for m in messages if m.get("id")}
mail_message_ids = {m.get('id') for m in messages if m.get('id')}
mail_messages = self.browse(mail_message_ids) mail_messages = self.browse(mail_message_ids)
tracking_statuses = mail_messages.tracking_status() tracking_statuses = mail_messages.tracking_status()
for message_dict in messages: for message_dict in messages:
mail_message_id = message_dict.get('id', False) mail_message_id = message_dict.get("id", False)
if mail_message_id: if mail_message_id:
message_dict.update(tracking_statuses[mail_message_id]) message_dict.update(tracking_statuses[mail_message_id])
return res return res
@ -180,27 +184,30 @@ class MailMessage(models.Model):
"""Preare values to be used by the chatter widget""" """Preare values to be used by the chatter widget"""
self.ensure_one() self.ensure_one()
failed_trackings = self.mail_tracking_ids.filtered( failed_trackings = self.mail_tracking_ids.filtered(
lambda x: x.state in self.get_failed_states()) lambda x: x.state in self.get_failed_states()
failed_partners = failed_trackings.mapped('partner_id') )
failed_partners = failed_trackings.mapped("partner_id")
failed_recipients = failed_partners.name_get() failed_recipients = failed_partners.name_get()
if self.author_id: if self.author_id:
author = self.author_id.name_get()[0] author = self.author_id.name_get()[0]
else: else:
author = (-1, _('-Unknown Author-')) author = (-1, _("-Unknown Author-"))
return { return {
'id': self.id, "id": self.id,
'date': self.date, "date": self.date,
'author': author, "author": author,
'body': self.body, "body": self.body,
'failed_recipients': failed_recipients, "failed_recipients": failed_recipients,
} }
@api.multi @api.multi
def get_failed_messages(self): def get_failed_messages(self):
"""Returns the list of failed messages to be used by the """Returns the list of failed messages to be used by the
failed_messages widget""" failed_messages widget"""
return [msg._prepare_dict_failed_message() return [
for msg in self.sorted('date', reverse=True)] msg._prepare_dict_failed_message()
for msg in self.sorted("date", reverse=True)
]
@api.multi @api.multi
def set_need_action_done(self): def set_need_action_done(self):
@ -208,23 +215,23 @@ class MailMessage(models.Model):
This will mark them to be ignored in the tracking issues filter. This will mark them to be ignored in the tracking issues filter.
""" """
self.check_access_rule('read') self.check_access_rule("read")
self.write({'mail_tracking_needs_action': False}) self.write({"mail_tracking_needs_action": False})
notification = { notification = {
'type': 'toggle_tracking_status', "type": "toggle_tracking_status",
'message_ids': self.ids, "message_ids": self.ids,
'needs_actions': False "needs_actions": False,
} }
self.env['bus.bus'].sendone( self.env["bus.bus"].sendone(
(self._cr.dbname, 'res.partner', self.env.user.partner_id.id), (self._cr.dbname, "res.partner", self.env.user.partner_id.id), notification
notification) )
def _get_failed_message_domain(self): def _get_failed_message_domain(self):
domain = self.env['mail.thread']._get_failed_message_domain() domain = self.env["mail.thread"]._get_failed_message_domain()
domain += [ domain += [
'|', "|",
('partner_ids', 'in', [self.env.user.partner_id.id]), ("partner_ids", "in", [self.env.user.partner_id.id]),
('author_id', '=', self.env.user.partner_id.id), ("author_id", "=", self.env.user.partner_id.id),
] ]
return domain return domain
@ -240,17 +247,16 @@ class MailMessage(models.Model):
Used by Discuss """ Used by Discuss """
unreviewed_messages = self.search(self._get_failed_message_domain()) unreviewed_messages = self.search(self._get_failed_message_domain())
unreviewed_messages.write({'mail_tracking_needs_action': False}) unreviewed_messages.write({"mail_tracking_needs_action": False})
ids = unreviewed_messages.ids ids = unreviewed_messages.ids
self.env['bus.bus'].sendone( self.env["bus.bus"].sendone(
(self._cr.dbname, 'res.partner', (self._cr.dbname, "res.partner", self.env.user.partner_id.id),
self.env.user.partner_id.id),
{ {
'type': 'toggle_tracking_status', "type": "toggle_tracking_status",
'message_ids': ids, "message_ids": ids,
'needs_actions': False, "needs_actions": False,
} },
) )
return ids return ids

View File

@ -1,7 +1,7 @@
# Copyright 2019 Alexandre Díaz # Copyright 2019 Alexandre Díaz
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models, api from odoo import api, models
class MailResendMessage(models.TransientModel): class MailResendMessage(models.TransientModel):
@ -10,45 +10,53 @@ class MailResendMessage(models.TransientModel):
@api.model @api.model
def default_get(self, fields): def default_get(self, fields):
rec = super().default_get(fields) rec = super().default_get(fields)
message_id = self._context.get('mail_message_to_resend') message_id = self._context.get("mail_message_to_resend")
if message_id: if message_id:
MailMessageObj = self.env['mail.message'] MailMessageObj = self.env["mail.message"]
mail_message_id = MailMessageObj.browse(message_id) mail_message_id = MailMessageObj.browse(message_id)
failed_states = MailMessageObj.get_failed_states() failed_states = MailMessageObj.get_failed_states()
tracking_ids = mail_message_id.mail_tracking_ids.filtered( tracking_ids = mail_message_id.mail_tracking_ids.filtered(
lambda x: x.state in failed_states) lambda x: x.state in failed_states
)
if any(tracking_ids): if any(tracking_ids):
partner_ids = [(0, 0, { partner_ids = [
"partner_id": tracking.partner_id.id, (
"name": tracking.partner_id.name, 0,
"email": tracking.partner_id.email, 0,
"resend": True, {
"message": tracking.error_description, "partner_id": tracking.partner_id.id,
}) for tracking in tracking_ids] "name": tracking.partner_id.name,
rec['partner_ids'].extend(partner_ids) "email": tracking.partner_id.email,
"resend": True,
"message": tracking.error_description,
},
)
for tracking in tracking_ids
]
rec["partner_ids"].extend(partner_ids)
return rec return rec
@api.multi @api.multi
def resend_mail_action(self): def resend_mail_action(self):
for wizard in self: for wizard in self:
to_send = wizard.partner_ids.filtered("resend").mapped( to_send = wizard.partner_ids.filtered("resend").mapped("partner_id")
"partner_id")
if to_send: if to_send:
# Set as reviewed # Set as reviewed
wizard.mail_message_id.mail_tracking_needs_action = False wizard.mail_message_id.mail_tracking_needs_action = False
# Reset mail.tracking.email state # Reset mail.tracking.email state
tracking_ids = wizard.mail_message_id.mail_tracking_ids\ tracking_ids = wizard.mail_message_id.mail_tracking_ids.filtered(
.filtered(lambda x: x.partner_id in to_send) lambda x: x.partner_id in to_send
tracking_ids.write({'state': False}) )
tracking_ids.write({"state": False})
# Send bus notifications to update Discuss and # Send bus notifications to update Discuss and
# mail_failed_messages widget # mail_failed_messages widget
notification = { notification = {
'type': 'toggle_tracking_status', "type": "toggle_tracking_status",
'message_ids': [self.mail_message_id.id], "message_ids": [self.mail_message_id.id],
'needs_actions': False "needs_actions": False,
} }
self.env['bus.bus'].sendone( self.env["bus.bus"].sendone(
(self._cr.dbname, 'res.partner', (self._cr.dbname, "res.partner", self.env.user.partner_id.id),
self.env.user.partner_id.id), notification,
notification) )
super().resend_mail_action() super().resend_mail_action()

View File

@ -1,32 +1,36 @@
# Copyright 2019 Alexandre Díaz # Copyright 2019 Alexandre Díaz
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import fields, models, api, _
from email.utils import getaddresses from email.utils import getaddresses
from odoo.tools import email_split_and_format
from lxml import etree from lxml import etree
from odoo import _, api, fields, models
from odoo.tools import email_split_and_format
class MailThread(models.AbstractModel): class MailThread(models.AbstractModel):
_inherit = "mail.thread" _inherit = "mail.thread"
failed_message_ids = fields.One2many( failed_message_ids = fields.One2many(
'mail.message', 'res_id', string='Failed Messages', "mail.message",
domain=lambda self: "res_id",
[('model', '=', self._name)] string="Failed Messages",
+ self._get_failed_message_domain()) domain=lambda self: [("model", "=", self._name)]
+ self._get_failed_message_domain(),
)
def _get_failed_message_domain(self): def _get_failed_message_domain(self):
"""Domain used to display failed messages on the 'failed_messages' """Domain used to display failed messages on the 'failed_messages'
widget""" widget"""
failed_states = self.env['mail.message'].get_failed_states() failed_states = self.env["mail.message"].get_failed_states()
return [ return [
('mail_tracking_needs_action', '=', True), ("mail_tracking_needs_action", "=", True),
('mail_tracking_ids.state', 'in', list(failed_states)), ("mail_tracking_ids.state", "in", list(failed_states)),
] ]
@api.multi @api.multi
@api.returns('self', lambda value: value.id) @api.returns("self", lambda value: value.id)
def message_post(self, *args, **kwargs): def message_post(self, *args, **kwargs):
"""Adds CC recipient to the message. """Adds CC recipient to the message.
@ -34,11 +38,9 @@ class MailThread(models.AbstractModel):
this information its written into the mail.message record. this information its written into the mail.message record.
""" """
new_message = super().message_post(*args, **kwargs) new_message = super().message_post(*args, **kwargs)
email_cc = kwargs.get('cc') email_cc = kwargs.get("cc")
if email_cc: if email_cc:
new_message.sudo().write({ new_message.sudo().write({"email_cc": email_cc})
'email_cc': email_cc,
})
return new_message return new_message
@api.multi @api.multi
@ -48,71 +50,79 @@ class MailThread(models.AbstractModel):
If the recipient has a res.partner, use it. If the recipient has a res.partner, use it.
""" """
res = super().message_get_suggested_recipients() res = super().message_get_suggested_recipients()
ResPartnerObj = self.env['res.partner'] ResPartnerObj = self.env["res.partner"]
email_cc_formated_list = [] email_cc_formated_list = []
for record in self: for record in self:
emails_cc = record.message_ids.mapped('email_cc') emails_cc = record.message_ids.mapped("email_cc")
for email in emails_cc: for email in emails_cc:
email_cc_formated_list.extend(email_split_and_format(email)) email_cc_formated_list.extend(email_split_and_format(email))
email_cc_formated_list = set(email_cc_formated_list) email_cc_formated_list = set(email_cc_formated_list)
for cc in email_cc_formated_list: for cc in email_cc_formated_list:
email_parts = getaddresses([cc])[0] email_parts = getaddresses([cc])[0]
partner_id = record.message_partner_info_from_emails( partner_id = record._message_partner_info_from_emails([email_parts[1]])[
[email_parts[1]])[0].get('partner_id') 0
].get("partner_id")
if not partner_id: if not partner_id:
record._message_add_suggested_recipient( record._message_add_suggested_recipient(res, email=cc, reason=_("Cc"))
res, email=cc, reason=_('Cc'))
else: else:
partner = ResPartnerObj.browse(partner_id, self._prefetch) partner = ResPartnerObj.browse(partner_id, self._prefetch)
record._message_add_suggested_recipient( record._message_add_suggested_recipient(
res, partner=partner, reason=_('Cc')) res, partner=partner, reason=_("Cc")
)
return res return res
@api.model @api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, def fields_view_get(
submenu=False): self, view_id=None, view_type="form", toolbar=False, submenu=False
):
"""Add filters for failed messages. """Add filters for failed messages.
These filters will show up on any form or search views of any These filters will show up on any form or search views of any
model inheriting from ``mail.thread``. model inheriting from ``mail.thread``.
""" """
res = super().fields_view_get( res = super().fields_view_get(
view_id=view_id, view_type=view_type, toolbar=toolbar, view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu
submenu=submenu) )
if view_type not in {'search', 'form'}: if view_type not in {"search", "form"}:
return res return res
doc = etree.XML(res['arch']) doc = etree.XML(res["arch"])
if view_type == 'search': if view_type == "search":
# Modify view to add new filter element # Modify view to add new filter element
nodes = doc.xpath("//search") nodes = doc.xpath("//search")
if nodes: if nodes:
# Create filter element # Create filter element
new_filter = etree.Element( new_filter = etree.Element(
'filter', { "filter",
'string': _('Failed sent messages'), {
'name': "failed_message_ids", "string": _("Failed sent messages"),
'domain': str([ "name": "failed_message_ids",
['failed_message_ids.mail_tracking_ids.state', "domain": str(
'in', [
list( [
self.env['mail.message'].get_failed_states() "failed_message_ids.mail_tracking_ids.state",
)], "in",
['failed_message_ids.mail_tracking_needs_action', list(self.env["mail.message"].get_failed_states()),
'=', True] ],
]) [
}) "failed_message_ids.mail_tracking_needs_action",
nodes[0].append(etree.Element('separator')) "=",
True,
],
]
),
},
)
nodes[0].append(etree.Element("separator"))
nodes[0].append(new_filter) nodes[0].append(new_filter)
elif view_type == 'form': elif view_type == "form":
# Modify view to add new field element # Modify view to add new field element
nodes = doc.xpath( nodes = doc.xpath("//field[@name='message_ids' and @widget='mail_thread']")
"//field[@name='message_ids' and @widget='mail_thread']")
if nodes: if nodes:
# Create field # Create field
field_failed_messages = etree.Element('field', { field_failed_messages = etree.Element(
'name': 'failed_message_ids', "field",
'widget': 'mail_failed_message', {"name": "failed_message_ids", "widget": "mail_failed_message"},
}) )
nodes[0].addprevious(field_failed_messages) nodes[0].addprevious(field_failed_messages)
res['arch'] = etree.tostring(doc, encoding='unicode') res["arch"] = etree.tostring(doc, encoding="unicode")
return res return res

View File

@ -2,9 +2,9 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging import logging
import urllib.parse
import time
import re import re
import time
import urllib.parse
import uuid import uuid
from datetime import datetime from datetime import datetime
@ -19,9 +19,9 @@ EVENT_CLICK_DELTA = 5 # seconds
class MailTrackingEmail(models.Model): class MailTrackingEmail(models.Model):
_name = "mail.tracking.email" _name = "mail.tracking.email"
_order = 'time desc' _order = "time desc"
_rec_name = 'display_name' _rec_name = "display_name"
_description = 'MailTracking email' _description = "MailTracking email"
# This table is going to grow fast and to infinite, so we index: # This table is going to grow fast and to infinite, so we index:
# - name: Search in tree view # - name: Search in tree view
@ -30,127 +30,153 @@ class MailTrackingEmail(models.Model):
# - state: Search and group_by in tree view # - state: Search and group_by in tree view
name = fields.Char(string="Subject", readonly=True, index=True) name = fields.Char(string="Subject", readonly=True, index=True)
display_name = fields.Char( display_name = fields.Char(
string="Display name", readonly=True, store=True, string="Display name",
compute="_compute_tracking_display_name") readonly=True,
store=True,
compute="_compute_tracking_display_name",
)
timestamp = fields.Float( timestamp = fields.Float(
string='UTC timestamp', readonly=True, string='UTC timestamp', readonly=True,
digits=dp.get_precision('MailTracking Timestamp')) digits=dp.get_precision('MailTracking Timestamp'))
time = fields.Datetime(string="Time", readonly=True, index=True) time = fields.Datetime(string="Time", readonly=True, index=True)
date = fields.Date( date = fields.Date(
string="Date", readonly=True, compute="_compute_date", store=True) string="Date", readonly=True, compute="_compute_date", store=True
)
mail_message_id = fields.Many2one( mail_message_id = fields.Many2one(
string="Message", comodel_name='mail.message', readonly=True, string="Message", comodel_name="mail.message", readonly=True, index=True
index=True) )
mail_id = fields.Many2one( mail_id = fields.Many2one(string="Email", comodel_name="mail.mail", readonly=True)
string="Email", comodel_name='mail.mail', readonly=True)
partner_id = fields.Many2one( partner_id = fields.Many2one(
string="Partner", comodel_name='res.partner', readonly=True) string="Partner", comodel_name="res.partner", readonly=True
recipient = fields.Char(string='Recipient email', readonly=True) )
recipient = fields.Char(string="Recipient email", readonly=True)
recipient_address = fields.Char( recipient_address = fields.Char(
string='Recipient email address', readonly=True, store=True, string="Recipient email address",
compute='_compute_recipient_address', index=True) readonly=True,
sender = fields.Char(string='Sender email', readonly=True) store=True,
state = fields.Selection([ compute="_compute_recipient_address",
('error', 'Error'), index=True,
('deferred', 'Deferred'), )
('sent', 'Sent'), sender = fields.Char(string="Sender email", readonly=True)
('delivered', 'Delivered'), state = fields.Selection(
('opened', 'Opened'), [
('rejected', 'Rejected'), ("error", "Error"),
('spam', 'Spam'), ("deferred", "Deferred"),
('unsub', 'Unsubscribed'), ("sent", "Sent"),
('bounced', 'Bounced'), ("delivered", "Delivered"),
('soft-bounced', 'Soft bounced'), ("opened", "Opened"),
], string='State', index=True, readonly=True, default=False, ("rejected", "Rejected"),
("spam", "Spam"),
("unsub", "Unsubscribed"),
("bounced", "Bounced"),
("soft-bounced", "Soft bounced"),
],
string="State",
index=True,
readonly=True,
default=False,
help=" * The 'Error' status indicates that there was an error " help=" * The 'Error' status indicates that there was an error "
"when trying to sent the email, for example, " "when trying to sent the email, for example, "
"'No valid recipient'\n" "'No valid recipient'\n"
" * The 'Sent' status indicates that message was succesfully " " * The 'Sent' status indicates that message was succesfully "
"sent via outgoing email server (SMTP).\n" "sent via outgoing email server (SMTP).\n"
" * The 'Delivered' status indicates that message was " " * The 'Delivered' status indicates that message was "
"succesfully delivered to recipient Mail Exchange (MX) server.\n" "succesfully delivered to recipient Mail Exchange (MX) server.\n"
" * The 'Opened' status indicates that message was opened or " " * The 'Opened' status indicates that message was opened or "
"clicked by recipient.\n" "clicked by recipient.\n"
" * The 'Rejected' status indicates that recipient email " " * The 'Rejected' status indicates that recipient email "
"address is blacklisted by outgoing email server (SMTP). " "address is blacklisted by outgoing email server (SMTP). "
"It is recomended to delete this email address.\n" "It is recomended to delete this email address.\n"
" * The 'Spam' status indicates that outgoing email " " * The 'Spam' status indicates that outgoing email "
"server (SMTP) consider this message as spam.\n" "server (SMTP) consider this message as spam.\n"
" * The 'Unsubscribed' status indicates that recipient has " " * The 'Unsubscribed' status indicates that recipient has "
"requested to be unsubscribed from this message.\n" "requested to be unsubscribed from this message.\n"
" * The 'Bounced' status indicates that message was bounced " " * The 'Bounced' status indicates that message was bounced "
"by recipient Mail Exchange (MX) server.\n" "by recipient Mail Exchange (MX) server.\n"
" * The 'Soft bounced' status indicates that message was soft " " * The 'Soft bounced' status indicates that message was soft "
"bounced by recipient Mail Exchange (MX) server.\n") "bounced by recipient Mail Exchange (MX) server.\n",
error_smtp_server = fields.Char(string='Error SMTP server', readonly=True) )
error_type = fields.Char(string='Error type', readonly=True) error_smtp_server = fields.Char(string="Error SMTP server", readonly=True)
error_description = fields.Char( error_type = fields.Char(string="Error type", readonly=True)
string='Error description', readonly=True) error_description = fields.Char(string="Error description", readonly=True)
bounce_type = fields.Char(string='Bounce type', readonly=True) bounce_type = fields.Char(string="Bounce type", readonly=True)
bounce_description = fields.Char( bounce_description = fields.Char(string="Bounce description", readonly=True)
string='Bounce description', readonly=True)
tracking_event_ids = fields.One2many( tracking_event_ids = fields.One2many(
string="Tracking events", comodel_name='mail.tracking.event', string="Tracking events",
inverse_name='tracking_email_id', readonly=True) comodel_name="mail.tracking.event",
inverse_name="tracking_email_id",
readonly=True,
)
# Token isn't generated here to have compatibility with older trackings. # Token isn't generated here to have compatibility with older trackings.
# New trackings have token and older not # New trackings have token and older not
token = fields.Char(string="Security Token", readonly=True, token = fields.Char(
default=lambda s: uuid.uuid4().hex, string="Security Token",
groups="base.group_system") readonly=True,
default=lambda s: uuid.uuid4().hex,
groups="base.group_system",
)
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):
records = super().create(vals_list) records = super().create(vals_list)
failed_states = self.env['mail.message'].get_failed_states() failed_states = self.env["mail.message"].get_failed_states()
records \ records.filtered(lambda one: one.state in failed_states).mapped(
.filtered(lambda one: one.state in failed_states) \ "mail_message_id"
.mapped("mail_message_id") \ ).write({"mail_tracking_needs_action": True})
.write({'mail_tracking_needs_action': True})
return records return records
@api.multi @api.multi
def write(self, vals): def write(self, vals):
super().write(vals) super().write(vals)
state = vals.get('state') state = vals.get("state")
if state and state in self.env['mail.message'].get_failed_states(): if state and state in self.env["mail.message"].get_failed_states():
self.mapped('mail_message_id').write({ self.mapped("mail_message_id").write({"mail_tracking_needs_action": True})
'mail_tracking_needs_action': True,
})
@api.model @api.model
def email_is_bounced(self, email): def email_is_bounced(self, email):
if not email: if not email:
return False return False
res = self._email_last_tracking_state(email) res = self._email_last_tracking_state(email)
return res and res[0].get('state', '') in {'rejected', 'error', return res and res[0].get("state", "") in {
'spam', 'bounced'} "rejected",
"error",
"spam",
"bounced",
}
@api.model @api.model
def _email_last_tracking_state(self, email): def _email_last_tracking_state(self, email):
return self.search_read([('recipient_address', '=', email.lower())], return self.search_read(
['state'], limit=1, order='time DESC') [("recipient_address", "=", email.lower())],
["state"],
limit=1,
order="time DESC",
)
@api.model @api.model
def email_score_from_email(self, email): def email_score_from_email(self, email):
if not email: if not email:
return 0. return 0.0
data = self.read_group([('recipient_address', '=', email.lower())], data = self.read_group(
['recipient_address', 'state'], ['state']) [("recipient_address", "=", email.lower())],
mapped_data = {state['state']: state['state_count'] for state in data} ["recipient_address", "state"],
["state"],
)
mapped_data = {state["state"]: state["state_count"] for state in data}
return self.with_context(mt_states=mapped_data).email_score() return self.with_context(mt_states=mapped_data).email_score()
@api.model @api.model
def _email_score_weights(self): def _email_score_weights(self):
"""Default email score weights. Ready to be inherited""" """Default email score weights. Ready to be inherited"""
return { return {
'error': -50.0, "error": -50.0,
'rejected': -25.0, "rejected": -25.0,
'spam': -25.0, "spam": -25.0,
'bounced': -25.0, "bounced": -25.0,
'soft-bounced': -10.0, "soft-bounced": -10.0,
'unsub': -10.0, "unsub": -10.0,
'delivered': 1.0, "delivered": 1.0,
'opened': 5.0, "opened": 5.0,
} }
def email_score(self): def email_score(self):
@ -163,7 +189,7 @@ class MailTrackingEmail(models.Model):
""" """
weights = self._email_score_weights() weights = self._email_score_weights()
score = 50.0 score = 50.0
states = self.env.context.get('mt_states', False) states = self.env.context.get("mt_states", False)
if states: if states:
for state in states.keys(): for state in states.keys():
score += weights.get(state, 0.0) * states[state] score += weights.get(state, 0.0) * states[state]
@ -176,13 +202,12 @@ class MailTrackingEmail(models.Model):
score = 0.0 score = 0.0
return score return score
@api.depends('recipient') @api.depends("recipient")
def _compute_recipient_address(self): def _compute_recipient_address(self):
for email in self: for email in self:
is_empty_recipient = (not email.recipient is_empty_recipient = not email.recipient or "<False>" in email.recipient
or '<False>' in email.recipient)
if not is_empty_recipient: if not is_empty_recipient:
matches = re.search(r'<(.*@.*)>', email.recipient) matches = re.search(r"<(.*@.*)>", email.recipient)
if matches: if matches:
email.recipient_address = matches.group(1).lower() email.recipient_address = matches.group(1).lower()
else: else:
@ -190,46 +215,45 @@ class MailTrackingEmail(models.Model):
else: else:
email.recipient_address = False email.recipient_address = False
@api.depends('name', 'recipient') @api.depends("name", "recipient")
def _compute_tracking_display_name(self): def _compute_tracking_display_name(self):
for email in self: for email in self:
parts = [email.name or ''] parts = [email.name or ""]
if email.recipient: if email.recipient:
parts.append(email.recipient) parts.append(email.recipient)
email.display_name = ' - '.join(parts) email.display_name = " - ".join(parts)
@api.depends('time') @api.depends("time")
def _compute_date(self): def _compute_date(self):
for email in self: for email in self:
email.date = fields.Date.to_string( email.date = fields.Date.to_string(fields.Date.from_string(email.time))
fields.Date.from_string(email.time))
def _get_mail_tracking_img(self): def _get_mail_tracking_img(self):
m_config = self.env['ir.config_parameter'] m_config = self.env["ir.config_parameter"]
base_url = (m_config.get_param('mail_tracking.base.url') base_url = m_config.get_param("mail_tracking.base.url") or m_config.get_param(
or m_config.get_param('web.base.url')) "web.base.url"
)
if self.token: if self.token:
path_url = ( path_url = (
'mail/tracking/open/%(db)s/%(tracking_email_id)s/%(token)s/' "mail/tracking/open/%(db)s/%(tracking_email_id)s/%(token)s/"
'blank.gif' % { "blank.gif"
'db': self.env.cr.dbname, % {
'tracking_email_id': self.id, "db": self.env.cr.dbname,
'token': self.token, "tracking_email_id": self.id,
}) "token": self.token,
}
)
else: else:
# This is here for compatibility with older records # This is here for compatibility with older records
path_url = ( path_url = "mail/tracking/open/{db}/{tracking_email_id}/blank.gif".format(
'mail/tracking/open/%(db)s/%(tracking_email_id)s/blank.gif' % { db=self.env.cr.dbname, tracking_email_id=self.id
'db': self.env.cr.dbname, )
'tracking_email_id': self.id,
})
track_url = urllib.parse.urljoin(base_url, path_url) track_url = urllib.parse.urljoin(base_url, path_url)
return ( return (
'<img src="%(url)s" alt="" ' '<img src="%(url)s" alt="" '
'data-odoo-tracking-email="%(tracking_email_id)s"/>' % { 'data-odoo-tracking-email="%(tracking_email_id)s"/>'
'url': track_url, % {"url": track_url, "tracking_email_id": self.id}
'tracking_email_id': self.id, )
})
@api.multi @api.multi
def _partners_email_bounced_set(self, reason, event=None): def _partners_email_bounced_set(self, reason, event=None):
@ -237,32 +261,35 @@ class MailTrackingEmail(models.Model):
if event and event.recipient_address: if event and event.recipient_address:
recipients.append(event.recipient_address) recipients.append(event.recipient_address)
else: else:
recipients = [x for x in self.mapped('recipient_address') if x] recipients = [x for x in self.mapped("recipient_address") if x]
for recipient in recipients: for recipient in recipients:
self.env['res.partner'].search([ self.env["res.partner"].search(
('email', '=ilike', recipient) [("email", "=ilike", recipient)]
]).email_bounced_set(self, reason, event=event) ).email_bounced_set(self, reason, event=event)
@api.multi @api.multi
def smtp_error(self, mail_server, smtp_server, exception): def smtp_error(self, mail_server, smtp_server, exception):
values = { values = {"state": "error"}
'state': 'error', IrMailServer = self.env["ir.mail_server"]
} if (
IrMailServer = self.env['ir.mail_server'] str(exception) == IrMailServer.NO_VALID_RECIPIENT
if str(exception) == IrMailServer.NO_VALID_RECIPIENT \ and not self.recipient_address
and not self.recipient_address: ):
values.update({ values.update(
'error_type': 'no_recipient', {
'error_description': "error_type": "no_recipient",
"The partner doesn't have a defined email", "error_description": "The partner doesn't have a defined email",
}) }
)
else: else:
values.update({ values.update(
'error_smtp_server': tools.ustr(smtp_server), {
'error_type': exception.__class__.__name__, "error_smtp_server": tools.ustr(smtp_server),
'error_description': tools.ustr(exception), "error_type": exception.__class__.__name__,
}) "error_description": tools.ustr(exception),
self.sudo()._partners_email_bounced_set('error') }
)
self.sudo()._partners_email_bounced_set("error")
self.sudo().write(values) self.sudo().write(values)
@api.multi @api.multi
@ -270,14 +297,14 @@ class MailTrackingEmail(models.Model):
self.ensure_one() self.ensure_one()
tracking_url = self._get_mail_tracking_img() tracking_url = self._get_mail_tracking_img()
if tracking_url: if tracking_url:
content = email.get('body', '') content = email.get("body", "")
content = re.sub( content = re.sub(
r'<img[^>]*data-odoo-tracking-email=["\'][0-9]*["\'][^>]*>', r'<img[^>]*data-odoo-tracking-email=["\'][0-9]*["\'][^>]*>', "", content
'', content) )
body = tools.append_content_to_html( body = tools.append_content_to_html(
content, tracking_url, plaintext=False, content, tracking_url, plaintext=False, container_tag="div"
container_tag='div') )
email['body'] = body email["body"] = body
return email return email
def _message_partners_check(self, message, message_id): def _message_partners_check(self, message, message_id):
@ -294,70 +321,67 @@ class MailTrackingEmail(models.Model):
'needaction_partner_ids': [(4, self.partner_id.id)], 'needaction_partner_ids': [(4, self.partner_id.id)],
}) })
else: else:
mail_message.sudo().write({ mail_message.sudo().write({"partner_ids": [(4, self.partner_id.id)]})
'partner_ids': [(4, self.partner_id.id)],
})
return True return True
@api.multi @api.multi
def _tracking_sent_prepare(self, mail_server, smtp_server, message, def _tracking_sent_prepare(self, mail_server, smtp_server, message, message_id):
message_id):
self.ensure_one() self.ensure_one()
ts = time.time() ts = time.time()
dt = datetime.utcfromtimestamp(ts) dt = datetime.utcfromtimestamp(ts)
self._message_partners_check(message, message_id) self._message_partners_check(message, message_id)
self.sudo().write({'state': 'sent'}) self.sudo().write({"state": "sent"})
return { return {
'recipient': message['To'], "recipient": message["To"],
'timestamp': '%.6f' % ts, "timestamp": "%.6f" % ts,
'time': fields.Datetime.to_string(dt), "time": fields.Datetime.to_string(dt),
'tracking_email_id': self.id, "tracking_email_id": self.id,
'event_type': 'sent', "event_type": "sent",
'smtp_server': smtp_server, "smtp_server": smtp_server,
} }
def _event_prepare(self, event_type, metadata): def _event_prepare(self, event_type, metadata):
self.ensure_one() self.ensure_one()
m_event = self.env['mail.tracking.event'] m_event = self.env["mail.tracking.event"]
method = getattr(m_event, 'process_' + event_type, None) method = getattr(m_event, "process_" + event_type, None)
if method and hasattr(method, '__call__'): if method and callable(method):
return method(self, metadata) return method(self, metadata)
else: # pragma: no cover else: # pragma: no cover
_logger.info('Unknown event type: %s' % event_type) _logger.info("Unknown event type: %s" % event_type)
return False return False
def _concurrent_events(self, event_type, metadata): def _concurrent_events(self, event_type, metadata):
m_event = self.env['mail.tracking.event'] m_event = self.env["mail.tracking.event"]
self.ensure_one() self.ensure_one()
concurrent_event_ids = False concurrent_event_ids = False
if event_type in {'open', 'click'}: if event_type in {"open", "click"}:
ts = metadata.get('timestamp', time.time()) ts = metadata.get("timestamp", time.time())
delta = EVENT_OPEN_DELTA if event_type == 'open' \ delta = EVENT_OPEN_DELTA if event_type == "open" else EVENT_CLICK_DELTA
else EVENT_CLICK_DELTA
domain = [ domain = [
('timestamp', '>=', ts - delta), ("timestamp", ">=", ts - delta),
('timestamp', '<=', ts + delta), ("timestamp", "<=", ts + delta),
('tracking_email_id', '=', self.id), ("tracking_email_id", "=", self.id),
('event_type', '=', event_type), ("event_type", "=", event_type),
] ]
if event_type == 'click': if event_type == "click":
domain.append(('url', '=', metadata.get('url', False))) domain.append(("url", "=", metadata.get("url", False)))
concurrent_event_ids = m_event.search(domain) concurrent_event_ids = m_event.search(domain)
return concurrent_event_ids return concurrent_event_ids
@api.multi @api.multi
def event_create(self, event_type, metadata): def event_create(self, event_type, metadata):
event_ids = self.env['mail.tracking.event'] event_ids = self.env["mail.tracking.event"]
for tracking_email in self: for tracking_email in self:
other_ids = tracking_email._concurrent_events(event_type, metadata) other_ids = tracking_email._concurrent_events(event_type, metadata)
if not other_ids: if not other_ids:
vals = tracking_email._event_prepare(event_type, metadata) vals = tracking_email._event_prepare(event_type, metadata)
if vals: if vals:
events = event_ids.sudo().create(vals) events = event_ids.sudo().create(vals)
if event_type in {'hard_bounce', 'spam', 'reject'}: if event_type in {"hard_bounce", "spam", "reject"}:
for event in events: for event in events:
self.sudo()._partners_email_bounced_set( self.sudo()._partners_email_bounced_set(
event_type, event=event) event_type, event=event
)
event_ids += events event_ids += events
else: else:
_logger.debug("Concurrent event '%s' discarded", event_type) _logger.debug("Concurrent event '%s' discarded", event_type)
@ -369,4 +393,4 @@ class MailTrackingEmail(models.Model):
# - return 'OK' if processed # - return 'OK' if processed
# - return 'NONE' if this request is not for you # - return 'NONE' if this request is not for you
# - return 'ERROR' if any error # - return 'ERROR' if any error
return 'NONE' # pragma: no cover return "NONE" # pragma: no cover

View File

@ -11,55 +11,69 @@ import odoo.addons.decimal_precision as dp
class MailTrackingEvent(models.Model): class MailTrackingEvent(models.Model):
_name = "mail.tracking.event" _name = "mail.tracking.event"
_order = 'timestamp desc' _order = "timestamp desc"
_rec_name = 'event_type' _rec_name = "event_type"
_description = 'MailTracking event' _description = "MailTracking event"
recipient = fields.Char(string="Recipient", readonly=True) recipient = fields.Char(string="Recipient", readonly=True)
recipient_address = fields.Char( recipient_address = fields.Char(
string='Recipient email address', readonly=True, store=True, string="Recipient email address",
compute='_compute_recipient_address', index=True) readonly=True,
store=True,
compute="_compute_recipient_address",
index=True,
)
timestamp = fields.Float( timestamp = fields.Float(
string='UTC timestamp', readonly=True, string='UTC timestamp', readonly=True,
digits=dp.get_precision('MailTracking Timestamp')) digits=dp.get_precision('MailTracking Timestamp'))
time = fields.Datetime(string="Time", readonly=True) time = fields.Datetime(string="Time", readonly=True)
date = fields.Date( date = fields.Date(
string="Date", readonly=True, compute="_compute_date", store=True) string="Date", readonly=True, compute="_compute_date", store=True
)
tracking_email_id = fields.Many2one( tracking_email_id = fields.Many2one(
string='Message', readonly=True, required=True, ondelete='cascade', string="Message",
comodel_name='mail.tracking.email', index=True) readonly=True,
event_type = fields.Selection(string='Event type', selection=[ required=True,
('sent', 'Sent'), ondelete="cascade",
('delivered', 'Delivered'), comodel_name="mail.tracking.email",
('deferral', 'Deferral'), index=True,
('hard_bounce', 'Hard bounce'), )
('soft_bounce', 'Soft bounce'), event_type = fields.Selection(
('open', 'Open'), string="Event type",
('click', 'Clicked'), selection=[
('spam', 'Spam'), ("sent", "Sent"),
('unsub', 'Unsubscribed'), ("delivered", "Delivered"),
('reject', 'Rejected'), ("deferral", "Deferral"),
], readonly=True) ("hard_bounce", "Hard bounce"),
smtp_server = fields.Char(string='SMTP server', readonly=True) ("soft_bounce", "Soft bounce"),
url = fields.Char(string='Clicked URL', readonly=True) ("open", "Open"),
ip = fields.Char(string='User IP', readonly=True) ("click", "Clicked"),
user_agent = fields.Char(string='User agent', readonly=True) ("spam", "Spam"),
mobile = fields.Boolean(string='Is mobile?', readonly=True) ("unsub", "Unsubscribed"),
os_family = fields.Char(string='Operating system family', readonly=True) ("reject", "Rejected"),
ua_family = fields.Char(string='User agent family', readonly=True) ],
ua_type = fields.Char(string='User agent type', readonly=True) readonly=True,
user_country_id = fields.Many2one(string='User country', readonly=True, )
comodel_name='res.country') smtp_server = fields.Char(string="SMTP server", readonly=True)
error_type = fields.Char(string='Error type', readonly=True) url = fields.Char(string="Clicked URL", readonly=True)
error_description = fields.Char(string='Error description', readonly=True) ip = fields.Char(string="User IP", readonly=True)
error_details = fields.Text(string='Error details', readonly=True) user_agent = fields.Char(string="User agent", readonly=True)
mobile = fields.Boolean(string="Is mobile?", readonly=True)
os_family = fields.Char(string="Operating system family", readonly=True)
ua_family = fields.Char(string="User agent family", readonly=True)
ua_type = fields.Char(string="User agent type", readonly=True)
user_country_id = fields.Many2one(
string="User country", readonly=True, comodel_name="res.country"
)
error_type = fields.Char(string="Error type", readonly=True)
error_description = fields.Char(string="Error description", readonly=True)
error_details = fields.Text(string="Error details", readonly=True)
@api.multi @api.depends("recipient")
@api.depends('recipient')
def _compute_recipient_address(self): def _compute_recipient_address(self):
for email in self: for email in self:
if email.recipient: if email.recipient:
matches = re.search(r'<(.*@.*)>', email.recipient) matches = re.search(r"<(.*@.*)>", email.recipient)
if matches: if matches:
email.recipient_address = matches.group(1).lower() email.recipient_address = matches.group(1).lower()
else: else:
@ -68,85 +82,82 @@ class MailTrackingEvent(models.Model):
email.recipient_address = False email.recipient_address = False
@api.multi @api.multi
@api.depends('time') @api.depends("time")
def _compute_date(self): def _compute_date(self):
for email in self: for email in self:
email.date = fields.Date.to_string( email.date = fields.Date.to_string(fields.Date.from_string(email.time))
fields.Date.from_string(email.time))
def _process_data(self, tracking_email, metadata, event_type, state): def _process_data(self, tracking_email, metadata, event_type, state):
ts = time.time() ts = time.time()
dt = datetime.utcfromtimestamp(ts) dt = datetime.utcfromtimestamp(ts)
return { return {
'recipient': metadata.get('recipient', tracking_email.recipient), "recipient": metadata.get("recipient", tracking_email.recipient),
'timestamp': metadata.get('timestamp', ts), "timestamp": metadata.get("timestamp", ts),
'time': metadata.get('time', fields.Datetime.to_string(dt)), "time": metadata.get("time", fields.Datetime.to_string(dt)),
'date': metadata.get('date', fields.Date.to_string(dt)), "date": metadata.get("date", fields.Date.to_string(dt)),
'tracking_email_id': tracking_email.id, "tracking_email_id": tracking_email.id,
'event_type': event_type, "event_type": event_type,
'ip': metadata.get('ip', False), "ip": metadata.get("ip", False),
'url': metadata.get('url', False), "url": metadata.get("url", False),
'user_agent': metadata.get('user_agent', False), "user_agent": metadata.get("user_agent", False),
'mobile': metadata.get('mobile', False), "mobile": metadata.get("mobile", False),
'os_family': metadata.get('os_family', False), "os_family": metadata.get("os_family", False),
'ua_family': metadata.get('ua_family', False), "ua_family": metadata.get("ua_family", False),
'ua_type': metadata.get('ua_type', False), "ua_type": metadata.get("ua_type", False),
'user_country_id': metadata.get('user_country_id', False), "user_country_id": metadata.get("user_country_id", False),
'error_type': metadata.get('error_type', False), "error_type": metadata.get("error_type", False),
'error_description': metadata.get('error_description', False), "error_description": metadata.get("error_description", False),
'error_details': metadata.get('error_details', False), "error_details": metadata.get("error_details", False),
} }
def _process_status(self, tracking_email, metadata, event_type, state): def _process_status(self, tracking_email, metadata, event_type, state):
tracking_email.sudo().write({'state': state}) tracking_email.sudo().write({"state": state})
return self._process_data(tracking_email, metadata, event_type, state) return self._process_data(tracking_email, metadata, event_type, state)
def _process_bounce(self, tracking_email, metadata, event_type, state): def _process_bounce(self, tracking_email, metadata, event_type, state):
tracking_email.sudo().write({ tracking_email.sudo().write(
'state': state, {
'bounce_type': metadata.get('bounce_type', False), "state": state,
'bounce_description': metadata.get('bounce_description', False), "bounce_type": metadata.get("bounce_type", False),
}) "bounce_description": metadata.get("bounce_description", False),
}
)
return self._process_data(tracking_email, metadata, event_type, state) return self._process_data(tracking_email, metadata, event_type, state)
@api.model @api.model
def process_delivered(self, tracking_email, metadata): def process_delivered(self, tracking_email, metadata):
return self._process_status( return self._process_status(tracking_email, metadata, "delivered", "delivered")
tracking_email, metadata, 'delivered', 'delivered')
@api.model @api.model
def process_deferral(self, tracking_email, metadata): def process_deferral(self, tracking_email, metadata):
return self._process_status( return self._process_status(tracking_email, metadata, "deferral", "deferred")
tracking_email, metadata, 'deferral', 'deferred')
@api.model @api.model
def process_hard_bounce(self, tracking_email, metadata): def process_hard_bounce(self, tracking_email, metadata):
return self._process_bounce( return self._process_bounce(tracking_email, metadata, "hard_bounce", "bounced")
tracking_email, metadata, 'hard_bounce', 'bounced')
@api.model @api.model
def process_soft_bounce(self, tracking_email, metadata): def process_soft_bounce(self, tracking_email, metadata):
return self._process_bounce( return self._process_bounce(
tracking_email, metadata, 'soft_bounce', 'soft-bounced') tracking_email, metadata, "soft_bounce", "soft-bounced"
)
@api.model @api.model
def process_open(self, tracking_email, metadata): def process_open(self, tracking_email, metadata):
return self._process_status(tracking_email, metadata, 'open', 'opened') return self._process_status(tracking_email, metadata, "open", "opened")
@api.model @api.model
def process_click(self, tracking_email, metadata): def process_click(self, tracking_email, metadata):
return self._process_status( return self._process_status(tracking_email, metadata, "click", "opened")
tracking_email, metadata, 'click', 'opened')
@api.model @api.model
def process_spam(self, tracking_email, metadata): def process_spam(self, tracking_email, metadata):
return self._process_status(tracking_email, metadata, 'spam', 'spam') return self._process_status(tracking_email, metadata, "spam", "spam")
@api.model @api.model
def process_unsub(self, tracking_email, metadata): def process_unsub(self, tracking_email, metadata):
return self._process_status(tracking_email, metadata, 'unsub', 'unsub') return self._process_status(tracking_email, metadata, "unsub", "unsub")
@api.model @api.model
def process_reject(self, tracking_email, metadata): def process_reject(self, tracking_email, metadata):
return self._process_status( return self._process_status(tracking_email, metadata, "reject", "rejected")
tracking_email, metadata, 'reject', 'rejected')

View File

@ -1,20 +1,21 @@
# Copyright 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com> # Copyright 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models, api, fields from odoo import api, fields, models
class ResPartner(models.Model): class ResPartner(models.Model):
_name = 'res.partner' _name = "res.partner"
_inherit = ['res.partner', 'mail.bounced.mixin'] _inherit = ["res.partner", "mail.bounced.mixin"]
# tracking_emails_count and email_score are non-store fields in order # tracking_emails_count and email_score are non-store fields in order
# to improve performance # to improve performance
tracking_emails_count = fields.Integer( tracking_emails_count = fields.Integer(
compute='_compute_tracking_emails_count', readonly=True) compute="_compute_tracking_emails_count", readonly=True
email_score = fields.Float(compute='_compute_email_score', readonly=True) )
email_score = fields.Float(compute="_compute_email_score", readonly=True)
@api.depends('email') @api.depends("email")
def _compute_email_score(self): def _compute_email_score(self):
for partner in self.filtered('email'): for partner in self.filtered('email'):
partner.email_score = self.env['mail.tracking.email'].\ partner.email_score = self.env['mail.tracking.email'].\
@ -26,7 +27,7 @@ class ResPartner(models.Model):
for partner in self: for partner in self:
count = 0 count = 0
if partner.email: if partner.email:
count = self.env['mail.tracking.email'].search_count([ count = self.env["mail.tracking.email"].search_count(
('recipient_address', '=', partner.email.lower()) [("recipient_address", "=", partner.email.lower())]
]) )
partner.tracking_emails_count = count partner.tracking_emails_count = count

View File

@ -1,77 +1,78 @@
# Copyright 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com> # Copyright 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import mock
from odoo.tools import mute_logger
import time
import base64 import base64
import time
import mock
import psycopg2 import psycopg2
import psycopg2.errorcodes import psycopg2.errorcodes
from odoo import http
from odoo.tests.common import TransactionCase
from ..controllers.main import MailTrackingController, BLANK
from lxml import etree from lxml import etree
mock_send_email = ('odoo.addons.base.models.ir_mail_server.' from odoo import _, http
'IrMailServer.send_email') from odoo.tests.common import TransactionCase
from odoo.tools import mute_logger
from ..controllers.main import BLANK, MailTrackingController
mock_send_email = "odoo.addons.base.models.ir_mail_server." "IrMailServer.send_email"
class FakeUserAgent(object): class FakeUserAgent(object):
browser = 'Test browser' browser = "Test browser"
platform = 'Test platform' platform = "Test platform"
def __str__(self): def __str__(self):
"""Return name""" """Return name"""
return 'Test suite' return "Test suite"
class TestMailTracking(TransactionCase): class TestMailTracking(TransactionCase):
def setUp(self, *args, **kwargs): def setUp(self, *args, **kwargs):
super(TestMailTracking, self).setUp(*args, **kwargs) super(TestMailTracking, self).setUp(*args, **kwargs)
self.sender = self.env['res.partner'].create({ self.sender = self.env["res.partner"].create(
'name': 'Test sender', {"name": "Test sender", "email": "sender@example.com"}
'email': 'sender@example.com', )
}) self.recipient = self.env["res.partner"].create(
self.recipient = self.env['res.partner'].create({ {"name": "Test recipient", "email": "recipient@example.com"}
'name': 'Test recipient', )
'email': 'recipient@example.com',
})
self.last_request = http.request self.last_request = http.request
http.request = type('obj', (object,), { http.request = type(
'env': self.env, "obj",
'cr': self.env.cr, (object,),
'db': self.env.cr.dbname, {
'endpoint': type('obj', (object,), { "env": self.env,
'routing': [], "cr": self.env.cr,
}), "db": self.env.cr.dbname,
'httprequest': type('obj', (object,), { "endpoint": type("obj", (object,), {"routing": []}),
'remote_addr': '123.123.123.123', "httprequest": type(
'user_agent': FakeUserAgent(), "obj",
}), (object,),
}) {"remote_addr": "123.123.123.123", "user_agent": FakeUserAgent()},
),
},
)
def tearDown(self, *args, **kwargs): def tearDown(self, *args, **kwargs):
http.request = self.last_request http.request = self.last_request
return super(TestMailTracking, self).tearDown(*args, **kwargs) return super(TestMailTracking, self).tearDown(*args, **kwargs)
def test_empty_email(self): def test_empty_email(self):
self.recipient.write({'email_bounced': True}) self.recipient.write({"email_bounced": True})
self.recipient.write({'email': False}) self.recipient.write({"email": False})
self.assertEqual(False, self.recipient.email) self.assertEqual(False, self.recipient.email)
self.assertEqual(False, self.recipient.email_bounced) self.assertEqual(False, self.recipient.email_bounced)
self.recipient.write({'email_bounced': True}) self.recipient.write({"email_bounced": True})
self.recipient.write({'email': ''}) self.recipient.write({"email": ""})
self.assertEqual(False, self.recipient.email_bounced) self.assertEqual(False, self.recipient.email_bounced)
self.assertEqual(False, self.env["mail.tracking.email"].email_is_bounced(False))
self.assertEqual( self.assertEqual(
False, 0.0, self.env["mail.tracking.email"].email_score_from_email(False)
self.env['mail.tracking.email'].email_is_bounced(False)) )
self.assertEqual(
0.,
self.env['mail.tracking.email'].email_score_from_email(False))
def test_recipient_address_compute(self): def test_recipient_address_compute(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.write({'recipient': False}) tracking.write({"recipient": False})
self.assertEqual(False, tracking.recipient_address) self.assertEqual(False, tracking.recipient_address)
def test_message_post(self): def test_message_post(self):
@ -88,154 +89,164 @@ class TestMailTracking(TransactionCase):
}) })
message._notify(message, {}, force_send=True) message._notify(message, {}, force_send=True)
# Search tracking created # Search tracking created
tracking_email = self.env['mail.tracking.email'].search([ tracking_email = self.env["mail.tracking.email"].search(
('mail_message_id', '=', message.id), [
('partner_id', '=', self.recipient.id), ("mail_message_id", "=", message.id),
]) ("partner_id", "=", self.recipient.id),
]
)
# The tracking email must be sent # The tracking email must be sent
self.assertTrue(tracking_email) self.assertTrue(tracking_email)
self.assertEqual(tracking_email.state, 'sent') self.assertEqual(tracking_email.state, "sent")
# message_dict read by web interface # message_dict read by web interface
message_dict = message.message_format()[0] message_dict = message.message_format()[0]
self.assertTrue(len(message_dict['partner_ids']) > 0) self.assertTrue(len(message_dict["partner_ids"]) > 0)
# First partner is recipient # First partner is recipient
partner_id = message_dict['partner_ids'][0][0] partner_id = message_dict["partner_ids"][0][0]
self.assertEqual(partner_id, self.recipient.id) self.assertEqual(partner_id, self.recipient.id)
status = message_dict['partner_trackings'][0] status = message_dict["partner_trackings"][0]
# Tracking status must be sent and # Tracking status must be sent and
# mail tracking must be the one search before # mail tracking must be the one search before
self.assertEqual(status['status'], 'sent') self.assertEqual(status["status"], "sent")
self.assertEqual(status['tracking_id'], tracking_email.id) self.assertEqual(status["tracking_id"], tracking_email.id)
self.assertEqual(status['recipient'], self.recipient.display_name) self.assertEqual(status["recipient"], self.recipient.display_name)
self.assertEqual(status['partner_id'], self.recipient.id) self.assertEqual(status["partner_id"], self.recipient.id)
self.assertEqual(status['isCc'], False) self.assertEqual(status["isCc"], False)
# And now open the email # And now open the email
metadata = { metadata = {
'ip': '127.0.0.1', "ip": "127.0.0.1",
'user_agent': 'Odoo Test/1.0', "user_agent": "Odoo Test/1.0",
'os_family': 'linux', "os_family": "linux",
'ua_family': 'odoo', "ua_family": "odoo",
} }
tracking_email.event_create('open', metadata) tracking_email.event_create("open", metadata)
self.assertEqual(tracking_email.state, 'opened') self.assertEqual(tracking_email.state, "opened")
def test_message_post_partner_no_email(self): def test_message_post_partner_no_email(self):
# Create message with recipient without defined email # Create message with recipient without defined email
self.recipient.write({'email': False}) self.recipient.write({"email": False})
message = self.env['mail.message'].create({ message = self.env["mail.message"].create(
'subject': 'Message test', {
'author_id': self.sender.id, "subject": "Message test",
'email_from': self.sender.email, "author_id": self.sender.id,
'message_type': 'comment', "email_from": self.sender.email,
'model': 'res.partner', "message_type": "comment",
'res_id': self.recipient.id, "model": "res.partner",
'partner_ids': [(4, self.recipient.id)], "res_id": self.recipient.id,
'body': '<p>This is a test message</p>', "partner_ids": [(4, self.recipient.id)],
}) "body": "<p>This is a test message</p>",
}
)
message._notify(message, {}, force_send=True) message._notify(message, {}, force_send=True)
# Search tracking created # Search tracking created
tracking_email = self.env['mail.tracking.email'].search([ tracking_email = self.env["mail.tracking.email"].search(
('mail_message_id', '=', message.id), [
('partner_id', '=', self.recipient.id), ("mail_message_id", "=", message.id),
]) ("partner_id", "=", self.recipient.id),
]
)
# No email should generate a error state: no_recipient # No email should generate a error state: no_recipient
self.assertEqual(tracking_email.state, 'error') self.assertEqual(tracking_email.state, "error")
self.assertEqual(tracking_email.error_type, 'no_recipient') self.assertEqual(tracking_email.error_type, "no_recipient")
self.assertFalse(self.recipient.email_bounced) self.assertFalse(self.recipient.email_bounced)
def _check_partner_trackings(self, message): def _check_partner_trackings(self, message):
message_dict = message.message_format()[0] message_dict = message.message_format()[0]
self.assertEqual(len(message_dict['partner_trackings']), 3) self.assertEqual(len(message_dict["partner_trackings"]), 3)
# mail cc # mail cc
foundPartner = False foundPartner = False
foundNoPartner = False foundNoPartner = False
for tracking in message_dict['partner_trackings']: for tracking in message_dict["partner_trackings"]:
if tracking['partner_id'] == self.sender.id: if tracking["partner_id"] == self.sender.id:
foundPartner = True foundPartner = True
self.assertTrue(tracking['isCc']) self.assertTrue(tracking["isCc"])
elif tracking['recipient'] == 'unnamed@test.com': elif tracking["recipient"] == "unnamed@test.com":
foundNoPartner = True foundNoPartner = True
self.assertFalse(tracking['partner_id']) self.assertFalse(tracking["partner_id"])
self.assertTrue(tracking['isCc']) self.assertTrue(tracking["isCc"])
elif tracking['partner_id'] == self.recipient.id: elif tracking["partner_id"] == self.recipient.id:
self.assertFalse(tracking['isCc']) self.assertFalse(tracking["isCc"])
self.assertTrue(foundPartner) self.assertTrue(foundPartner)
self.assertTrue(foundNoPartner) self.assertTrue(foundNoPartner)
def test_email_cc(self): def test_email_cc(self):
sender_user = self.env['res.users'].create({ sender_user = self.env["res.users"].create(
'name': 'Sender User Test', {
'partner_id': self.sender.id, "name": "Sender User Test",
'login': 'sender-test', "partner_id": self.sender.id,
}) "login": "sender-test",
message = self.recipient.sudo(user=sender_user).message_post( }
body='<p>This is a test message</p>', )
cc='unnamed@test.com, sender@example.com' message = self.recipient.with_user(sender_user).message_post(
body=_("<p>This is a test message</p>"),
cc="unnamed@test.com, sender@example.com",
) )
# suggested recipients # suggested recipients
recipients = self.recipient.message_get_suggested_recipients() recipients = self.recipient._message_get_suggested_recipients()
suggested_mails = { suggested_mails = {email[1] for email in recipients[self.recipient.id]}
email[1] for email in recipients[self.recipient.id] self.assertTrue("unnamed@test.com" in suggested_mails)
}
self.assertTrue('unnamed@test.com' in suggested_mails)
self.assertEqual(len(recipients[self.recipient.id][0]), 3) self.assertEqual(len(recipients[self.recipient.id][0]), 3)
# Repeated Cc recipients # Repeated Cc recipients
message = self.env['mail.message'].create({ message = self.env["mail.message"].create(
'subject': 'Message test', {
'author_id': self.sender.id, "subject": "Message test",
'email_from': self.sender.email, "author_id": self.sender.id,
'message_type': 'comment', "email_from": self.sender.email,
'model': 'res.partner', "message_type": "comment",
'res_id': self.recipient.id, "model": "res.partner",
'partner_ids': [(4, self.recipient.id)], "res_id": self.recipient.id,
'email_cc': 'unnamed@test.com, sender@example.com' "partner_ids": [(4, self.recipient.id)],
', recipient@example.com', "email_cc": "unnamed@test.com, sender@example.com"
'body': '<p>This is another test message</p>', ", recipient@example.com",
}) "body": "<p>This is another test message</p>",
}
)
message._notify(message, {}, force_send=True) message._notify(message, {}, force_send=True)
recipients = self.recipient.message_get_suggested_recipients() recipients = self.recipient._message_get_suggested_recipients()
self.assertEqual(len(recipients[self.recipient.id][0]), 3) self.assertEqual(len(recipients[self.recipient.id][0]), 3)
self._check_partner_trackings(message) self._check_partner_trackings(message)
def test_failed_message(self): def test_failed_message(self):
MailMessageObj = self.env['mail.message'] MailMessageObj = self.env["mail.message"]
# Create message # Create message
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
self.assertFalse(tracking.mail_message_id.mail_tracking_needs_action) self.assertFalse(tracking.mail_message_id.mail_tracking_needs_action)
# Force error state # Force error state
tracking.state = 'error' tracking.state = "error"
self.assertTrue(tracking.mail_message_id.mail_tracking_needs_action) self.assertTrue(tracking.mail_message_id.mail_tracking_needs_action)
failed_count = MailMessageObj.get_failed_count() failed_count = MailMessageObj.get_failed_count()
self.assertTrue(failed_count > 0) self.assertTrue(failed_count > 0)
values = tracking.mail_message_id.get_failed_messages() values = tracking.mail_message_id.get_failed_messages()
self.assertEqual(values[0]['id'], tracking.mail_message_id.id) self.assertEqual(values[0]["id"], tracking.mail_message_id.id)
messages = MailMessageObj.search([]) messages = MailMessageObj.search([])
messages_failed = MailMessageObj.search( messages_failed = MailMessageObj.search(
MailMessageObj._get_failed_message_domain()) MailMessageObj._get_failed_message_domain()
)
self.assertTrue(messages) self.assertTrue(messages)
self.assertTrue(messages_failed) self.assertTrue(messages_failed)
self.assertTrue(len(messages) > len(messages_failed)) self.assertTrue(len(messages) > len(messages_failed))
tracking.mail_message_id.set_need_action_done() tracking.mail_message_id.set_need_action_done()
self.assertFalse(tracking.mail_message_id.mail_tracking_needs_action) self.assertFalse(tracking.mail_message_id.mail_tracking_needs_action)
self.assertTrue( self.assertTrue(MailMessageObj.get_failed_count() < failed_count)
MailMessageObj.get_failed_count() < failed_count)
# No author_id # No author_id
tracking.mail_message_id.author_id = False tracking.mail_message_id.author_id = False
values = tracking.mail_message_id.get_failed_messages()[0] values = tracking.mail_message_id.get_failed_messages()[0]
self.assertEqual(values['author'][0], -1) self.assertEqual(values["author"][0], -1)
def mail_send(self, recipient): def mail_send(self, recipient):
mail = self.env['mail.mail'].create({ mail = self.env["mail.mail"].create(
'subject': 'Test subject', {
'email_from': 'from@domain.com', "subject": "Test subject",
'email_to': recipient, "email_from": "from@domain.com",
'body_html': '<p>This is a test message</p>', "email_to": recipient,
}) "body_html": "<p>This is a test message</p>",
}
)
mail.send() mail.send()
# Search tracking created # Search tracking created
tracking_email = self.env['mail.tracking.email'].search([ tracking_email = self.env["mail.tracking.email"].search(
('mail_id', '=', mail.id), [("mail_id", "=", mail.id)]
]) )
return mail, tracking_email return mail, tracking_email
def test_mail_send(self): def test_mail_send(self):
@ -245,10 +256,9 @@ class TestMailTracking(TransactionCase):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
self.assertEqual(mail.email_to, tracking.recipient) self.assertEqual(mail.email_to, tracking.recipient)
self.assertEqual(mail.email_from, tracking.sender) self.assertEqual(mail.email_from, tracking.sender)
with mock.patch('odoo.http.db_filter') as mock_client: with mock.patch("odoo.http.db_filter") as mock_client:
mock_client.return_value = True mock_client.return_value = True
res = controller.mail_tracking_open( res = controller.mail_tracking_open(db, tracking.id, tracking.token)
db, tracking.id, tracking.token)
self.assertEqual(image, res.response[0]) self.assertEqual(image, res.response[0])
# Two events: sent and open # Two events: sent and open
self.assertEqual(2, len(tracking.tracking_event_ids)) self.assertEqual(2, len(tracking.tracking_event_ids))
@ -261,30 +271,30 @@ class TestMailTracking(TransactionCase):
def test_mail_tracking_open(self): def test_mail_tracking_open(self):
controller = MailTrackingController() controller = MailTrackingController()
db = self.env.cr.dbname db = self.env.cr.dbname
with mock.patch('odoo.http.db_filter') as mock_client: with mock.patch("odoo.http.db_filter") as mock_client:
mock_client.return_value = True mock_client.return_value = True
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
# Tracking is in sent or delivered state. But no token give. # Tracking is in sent or delivered state. But no token give.
# Don't generates tracking event # Don't generates tracking event
controller.mail_tracking_open(db, tracking.id) controller.mail_tracking_open(db, tracking.id)
self.assertEqual(1, len(tracking.tracking_event_ids)) self.assertEqual(1, len(tracking.tracking_event_ids))
tracking.write({'state': 'opened'}) tracking.write({"state": "opened"})
# Tracking isn't in sent or delivered state. # Tracking isn't in sent or delivered state.
# Don't generates tracking event # Don't generates tracking event
controller.mail_tracking_open(db, tracking.id, tracking.token) controller.mail_tracking_open(db, tracking.id, tracking.token)
self.assertEqual(1, len(tracking.tracking_event_ids)) self.assertEqual(1, len(tracking.tracking_event_ids))
tracking.write({'state': 'sent'}) tracking.write({"state": "sent"})
# Tracking is in sent or delivered state and a token is given. # Tracking is in sent or delivered state and a token is given.
# Generates tracking event # Generates tracking event
controller.mail_tracking_open(db, tracking.id, tracking.token) controller.mail_tracking_open(db, tracking.id, tracking.token)
self.assertEqual(2, len(tracking.tracking_event_ids)) self.assertEqual(2, len(tracking.tracking_event_ids))
# Generate new email due concurrent event filter # Generate new email due concurrent event filter
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.write({'token': False}) tracking.write({"token": False})
# Tracking is in sent or delivered state but a token is given for a # Tracking is in sent or delivered state but a token is given for a
# record that doesn't have a token. # record that doesn't have a token.
# Don't generates tracking event # Don't generates tracking event
controller.mail_tracking_open(db, tracking.id, 'tokentest') controller.mail_tracking_open(db, tracking.id, "tokentest")
self.assertEqual(1, len(tracking.tracking_event_ids)) self.assertEqual(1, len(tracking.tracking_event_ids))
# Tracking is in sent or delivered state and not token is given for # Tracking is in sent or delivered state and not token is given for
# a record that doesn't have a token. # a record that doesn't have a token.
@ -296,90 +306,76 @@ class TestMailTracking(TransactionCase):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
ts = time.time() ts = time.time()
metadata = { metadata = {
'ip': '127.0.0.1', "ip": "127.0.0.1",
'user_agent': 'Odoo Test/1.0', "user_agent": "Odoo Test/1.0",
'os_family': 'linux', "os_family": "linux",
'ua_family': 'odoo', "ua_family": "odoo",
'timestamp': ts, "timestamp": ts,
} }
# First open event # First open event
tracking.event_create('open', metadata) tracking.event_create("open", metadata)
opens = tracking.tracking_event_ids.filtered( opens = tracking.tracking_event_ids.filtered(lambda r: r.event_type == "open")
lambda r: r.event_type == 'open'
)
self.assertEqual(len(opens), 1) self.assertEqual(len(opens), 1)
# Concurrent open event # Concurrent open event
metadata['timestamp'] = ts + 2 metadata["timestamp"] = ts + 2
tracking.event_create('open', metadata) tracking.event_create("open", metadata)
opens = tracking.tracking_event_ids.filtered( opens = tracking.tracking_event_ids.filtered(lambda r: r.event_type == "open")
lambda r: r.event_type == 'open'
)
self.assertEqual(len(opens), 1) self.assertEqual(len(opens), 1)
# Second open event # Second open event
metadata['timestamp'] = ts + 350 metadata["timestamp"] = ts + 350
tracking.event_create('open', metadata) tracking.event_create("open", metadata)
opens = tracking.tracking_event_ids.filtered( opens = tracking.tracking_event_ids.filtered(lambda r: r.event_type == "open")
lambda r: r.event_type == 'open'
)
self.assertEqual(len(opens), 2) self.assertEqual(len(opens), 2)
def test_concurrent_click(self): def test_concurrent_click(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
ts = time.time() ts = time.time()
metadata = { metadata = {
'ip': '127.0.0.1', "ip": "127.0.0.1",
'user_agent': 'Odoo Test/1.0', "user_agent": "Odoo Test/1.0",
'os_family': 'linux', "os_family": "linux",
'ua_family': 'odoo', "ua_family": "odoo",
'timestamp': ts, "timestamp": ts,
'url': 'https://www.example.com/route/1', "url": "https://www.example.com/route/1",
} }
# First click event (URL 1) # First click event (URL 1)
tracking.event_create('click', metadata) tracking.event_create("click", metadata)
opens = tracking.tracking_event_ids.filtered( opens = tracking.tracking_event_ids.filtered(lambda r: r.event_type == "click")
lambda r: r.event_type == 'click'
)
self.assertEqual(len(opens), 1) self.assertEqual(len(opens), 1)
# Concurrent click event (URL 1) # Concurrent click event (URL 1)
metadata['timestamp'] = ts + 2 metadata["timestamp"] = ts + 2
tracking.event_create('click', metadata) tracking.event_create("click", metadata)
opens = tracking.tracking_event_ids.filtered( opens = tracking.tracking_event_ids.filtered(lambda r: r.event_type == "click")
lambda r: r.event_type == 'click'
)
self.assertEqual(len(opens), 1) self.assertEqual(len(opens), 1)
# Second click event (URL 1) # Second click event (URL 1)
metadata['timestamp'] = ts + 350 metadata["timestamp"] = ts + 350
tracking.event_create('click', metadata) tracking.event_create("click", metadata)
opens = tracking.tracking_event_ids.filtered( opens = tracking.tracking_event_ids.filtered(lambda r: r.event_type == "click")
lambda r: r.event_type == 'click'
)
self.assertEqual(len(opens), 2) self.assertEqual(len(opens), 2)
# Concurrent click event (URL 2) # Concurrent click event (URL 2)
metadata['timestamp'] = ts + 2 metadata["timestamp"] = ts + 2
metadata['url'] = 'https://www.example.com/route/2' metadata["url"] = "https://www.example.com/route/2"
tracking.event_create('click', metadata) tracking.event_create("click", metadata)
opens = tracking.tracking_event_ids.filtered( opens = tracking.tracking_event_ids.filtered(lambda r: r.event_type == "click")
lambda r: r.event_type == 'click'
)
self.assertEqual(len(opens), 3) self.assertEqual(len(opens), 3)
@mute_logger('odoo.addons.mail.models.mail_mail') @mute_logger("odoo.addons.mail.models.mail_mail")
def test_smtp_error(self): def test_smtp_error(self):
with mock.patch(mock_send_email) as mock_func: with mock.patch(mock_send_email) as mock_func:
mock_func.side_effect = Warning('Test error') mock_func.side_effect = Warning("Test error")
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
self.assertEqual('error', tracking.state) self.assertEqual("error", tracking.state)
self.assertEqual('Warning', tracking.error_type) self.assertEqual("Warning", tracking.error_type)
self.assertEqual('Test error', tracking.error_description) self.assertEqual("Test error", tracking.error_description)
self.assertTrue(self.recipient.email_bounced) self.assertTrue(self.recipient.email_bounced)
def test_partner_email_change(self): def test_partner_email_change(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('open', {}) tracking.event_create("open", {})
orig_score = self.recipient.email_score orig_score = self.recipient.email_score
orig_count = self.recipient.tracking_emails_count orig_count = self.recipient.tracking_emails_count
orig_email = self.recipient.email orig_email = self.recipient.email
self.recipient.email = orig_email + '2' self.recipient.email = orig_email + "2"
self.assertEqual(50.0, self.recipient.email_score) self.assertEqual(50.0, self.recipient.email_score)
self.assertEqual(0, self.recipient.tracking_emails_count) self.assertEqual(0, self.recipient.tracking_emails_count)
self.recipient.email = orig_email self.recipient.email = orig_email
@ -388,108 +384,106 @@ class TestMailTracking(TransactionCase):
def test_process_hard_bounce(self): def test_process_hard_bounce(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('hard_bounce', {}) tracking.event_create("hard_bounce", {})
self.assertEqual('bounced', tracking.state) self.assertEqual("bounced", tracking.state)
self.assertTrue(self.recipient.email_score < 50.0) self.assertTrue(self.recipient.email_score < 50.0)
def test_process_soft_bounce(self): def test_process_soft_bounce(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('soft_bounce', {}) tracking.event_create("soft_bounce", {})
self.assertEqual('soft-bounced', tracking.state) self.assertEqual("soft-bounced", tracking.state)
self.assertTrue(self.recipient.email_score < 50.0) self.assertTrue(self.recipient.email_score < 50.0)
def test_process_delivered(self): def test_process_delivered(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('delivered', {}) tracking.event_create("delivered", {})
self.assertEqual('delivered', tracking.state) self.assertEqual("delivered", tracking.state)
self.assertTrue(self.recipient.email_score > 50.0) self.assertTrue(self.recipient.email_score > 50.0)
def test_process_deferral(self): def test_process_deferral(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('deferral', {}) tracking.event_create("deferral", {})
self.assertEqual('deferred', tracking.state) self.assertEqual("deferred", tracking.state)
def test_process_spam(self): def test_process_spam(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('spam', {}) tracking.event_create("spam", {})
self.assertEqual('spam', tracking.state) self.assertEqual("spam", tracking.state)
self.assertTrue(self.recipient.email_score < 50.0) self.assertTrue(self.recipient.email_score < 50.0)
def test_process_unsub(self): def test_process_unsub(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('unsub', {}) tracking.event_create("unsub", {})
self.assertEqual('unsub', tracking.state) self.assertEqual("unsub", tracking.state)
self.assertTrue(self.recipient.email_score < 50.0) self.assertTrue(self.recipient.email_score < 50.0)
def test_process_reject(self): def test_process_reject(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('reject', {}) tracking.event_create("reject", {})
self.assertEqual('rejected', tracking.state) self.assertEqual("rejected", tracking.state)
self.assertTrue(self.recipient.email_score < 50.0) self.assertTrue(self.recipient.email_score < 50.0)
def test_process_open(self): def test_process_open(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('open', {}) tracking.event_create("open", {})
self.assertEqual('opened', tracking.state) self.assertEqual("opened", tracking.state)
self.assertTrue(self.recipient.email_score > 50.0) self.assertTrue(self.recipient.email_score > 50.0)
def test_process_click(self): def test_process_click(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('click', {}) tracking.event_create("click", {})
self.assertEqual('opened', tracking.state) self.assertEqual("opened", tracking.state)
self.assertTrue(self.recipient.email_score > 50.0) self.assertTrue(self.recipient.email_score > 50.0)
def test_process_several_bounce(self): def test_process_several_bounce(self):
for i in range(1, 10): for _i in range(1, 10):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('hard_bounce', {}) tracking.event_create("hard_bounce", {})
self.assertEqual('bounced', tracking.state) self.assertEqual("bounced", tracking.state)
self.assertEqual(0.0, self.recipient.email_score) self.assertEqual(0.0, self.recipient.email_score)
def test_bounce_new_partner(self): def test_bounce_new_partner(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('hard_bounce', {}) tracking.event_create("hard_bounce", {})
new_partner = self.env['res.partner'].create({ new_partner = self.env["res.partner"].create({"name": "Test New Partner"})
'name': 'Test New Partner',
})
new_partner.email = self.recipient.email new_partner.email = self.recipient.email
self.assertTrue(new_partner.email_bounced) self.assertTrue(new_partner.email_bounced)
def test_recordset_email_score(self): def test_recordset_email_score(self):
"""For backwords compatibility sake""" """For backwords compatibility sake"""
trackings = self.env['mail.tracking.email'] trackings = self.env["mail.tracking.email"]
for i in range(11): for _i in range(11):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('click', {}) tracking.event_create("click", {})
trackings |= tracking trackings |= tracking
self.assertEqual(100.0, trackings.email_score()) self.assertEqual(100.0, trackings.email_score())
def test_db(self): def test_db(self):
db = self.env.cr.dbname db = self.env.cr.dbname
controller = MailTrackingController() controller = MailTrackingController()
with mock.patch('odoo.http.db_filter') as mock_client: with mock.patch("odoo.http.db_filter") as mock_client:
mock_client.return_value = True mock_client.return_value = True
with self.assertRaises(psycopg2.OperationalError): with self.assertRaises(psycopg2.OperationalError):
controller.mail_tracking_event('not_found_db') controller.mail_tracking_event("not_found_db")
none = controller.mail_tracking_event(db) none = controller.mail_tracking_event(db)
self.assertEqual(b'NONE', none.response[0]) self.assertEqual(b"NONE", none.response[0])
none = controller.mail_tracking_event(db, 'open') none = controller.mail_tracking_event(db, "open")
self.assertEqual(b'NONE', none.response[0]) self.assertEqual(b"NONE", none.response[0])
class TestMailTrackingViews(TransactionCase): class TestMailTrackingViews(TransactionCase):
def test_fields_view_get(self): def test_fields_view_get(self):
result = self.env['res.partner'].fields_view_get( result = self.env["res.partner"].fields_view_get(
view_id=self.env.ref('base.view_partner_form').id, view_id=self.env.ref("base.view_partner_form").id, view_type="form"
view_type='form') )
doc = etree.XML(result['arch']) doc = etree.XML(result["arch"])
nodes = doc.xpath( nodes = doc.xpath(
"//field[@name='failed_message_ids'" "//field[@name='failed_message_ids'" " and @widget='mail_failed_message']"
" and @widget='mail_failed_message']") )
self.assertTrue(nodes) self.assertTrue(nodes)
result = self.env['res.partner'].fields_view_get( result = self.env["res.partner"].fields_view_get(
view_id=self.env.ref('base.view_res_partner_filter').id, view_id=self.env.ref("base.view_res_partner_filter").id, view_type="search"
view_type='search') )
doc = etree.XML(result['arch']) doc = etree.XML(result["arch"])
nodes = doc.xpath("//filter[@name='failed_message_ids']") nodes = doc.xpath("//filter[@name='failed_message_ids']")
self.assertTrue(nodes) self.assertTrue(nodes)