[IMP] mail_tracking: black, isort
This commit is contained in:
parent
9d4c4d3533
commit
200e016ab8
@ -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",
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user