social/mail_tracking_mailgun/models/res_partner.py
Jairo Llopis 99836513b9 [IMP] mail_tracking_mailgun: refactor to support modern webhooks
Before this patch, the module was designed after the [deprecated Mailgun webhooks][3]. However Mailgun had the [events API][2] which was quite different. Modern Mailgun has deprecated those webhooks and instead uses new ones that include the same payload as the events API, so you can reuse code.

However, this was incorrectly reusing the code inversely: trying to process the events API through the same code prepared for the deprecated webhooks.

Besides, both `failed` and `rejected` mailgun events were mapped to `error` state, but that was also wrong because [`mail_tracking` doesn't have an `error` state][1].

So the logic of the whole module is changed, adapting it to process the events API payload, both through controllers (prepared for the new webhooks) and manual updates that directly call the events API.

Also, `rejected` is now translated into `reject`, and `failed` is translated into `hard_bounce` or `soft_bounce` depending on the severity, as specified by [mailgun docs][2]. Also, `bounced` and `dropped` mailgun states are removed because they don't exist, and instead `failed` and `rejected` properly get their metadata.

Of course, to know the severity, now the method to obtain that info must change, it' can't be a simple dict anymore.

Added more parameters because for example modern Mailgun uses different keys for signing payload than for accessing the API. As there are so many parameters, configuration is now possible through `res.config.settings`. Go there to autoregister webhooks too.

Since the new webhooks are completely incompatible with the old supposedly-abstract webhooks controllers (that were never really that abstract), support for old webhooks is removed, and it will be removed in the future from `mail_tracking` directly. There is a migration script that attempts to unregister old webhooks and register new ones automatically.

[1]: f73de421e2/mail_tracking/models/mail_tracking_event.py (L31-L42)
[2]: https://documentation.mailgun.com/en/latest/api-events.html#event-types
[3]: https://documentation.mailgun.com/en/latest/api-webhooks-deprecated.html
2022-05-17 17:16:14 -03:00

194 lines
7.3 KiB
Python

# Copyright 2016 Tecnativa - Antonio Espinosa
# Copyright 2016 Tecnativa - Carlos Dauden
# Copyright 2017 Tecnativa - Pedro M. Baeza
# Copyright 2017 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from urllib.parse import urljoin
import requests
from odoo import _, api, models
from odoo.exceptions import UserError
class ResPartner(models.Model):
_inherit = "res.partner"
def email_bounced_set(self, tracking_emails, reason, event=None):
res = super().email_bounced_set(tracking_emails, reason, event=event)
self._email_bounced_set(reason, event)
return res
def _email_bounced_set(self, reason, event):
for partner in self:
if not partner.email:
continue
event = event or self.env["mail.tracking.event"]
event_str = """
<a href="#"
data-oe-model="mail.tracking.event" data-oe-id="%d">%s</a>
""" % (
event.id or 0,
event.id or _("unknown"),
)
body = _("Email has been bounced: %s\nReason: %s\nEvent: %s") % (
partner.email,
reason,
event_str,
)
partner.message_post(body=body)
def check_email_validity(self):
"""
Checks mailbox validity with Mailgun's API
API documentation:
https://documentation.mailgun.com/en/latest/api-email-validation.html
"""
params = self.env["mail.tracking.email"]._mailgun_values()
if not params.validation_key:
raise UserError(
_(
"You need to configure mailgun.validation_key"
" in order to be able to check mails validity"
)
)
for partner in self.filtered("email"):
res = requests.get(
urljoin(params.api_url, "/v3/address/validate"),
auth=("api", params.validation_key),
params={"address": partner.email, "mailbox_verification": True},
)
if (
not res
or res.status_code != 200
and not self.env.context.get("mailgun_auto_check")
):
raise UserError(
_(
"Error %s trying to check mail" % res.status_code
or "of connection"
)
)
content = res.json()
if "mailbox_verification" not in content:
if not self.env.context.get("mailgun_auto_check"):
raise UserError(
_(
"Mailgun Error. Mailbox verification value wasn't"
" returned"
)
)
# Not a valid address: API sets 'is_valid' as False
# and 'mailbox_verification' as None
if not content["is_valid"]:
partner.email_bounced = True
body = (
_(
"%s is not a valid email address. Please check it"
" in order to avoid sending issues"
)
% partner.email
)
if not self.env.context.get("mailgun_auto_check"):
raise UserError(body)
partner.message_post(body=body)
# If the mailbox is not valid API returns 'mailbox_verification'
# as a string with value 'false'
if content["mailbox_verification"] == "false":
partner.email_bounced = True
body = (
_(
"%s failed the mailbox verification. Please check it"
" in order to avoid sending issues"
)
% partner.email
)
if not self.env.context.get("mailgun_auto_check"):
raise UserError(body)
partner.message_post(body=body)
# If Mailgun can't complete the validation request the API returns
# 'mailbox_verification' as a string set to 'unknown'
if content["mailbox_verification"] == "unknown":
if not self.env.context.get("mailgun_auto_check"):
raise UserError(
_(
"%s couldn't be verified. Either the request couln't"
" be completed or the mailbox provider doesn't "
"support email verification"
)
% (partner.email)
)
def check_email_bounced(self):
"""
Checks if the partner's email is in Mailgun's bounces list
API documentation:
https://documentation.mailgun.com/en/latest/api-suppressions.html
"""
api_key, api_url, domain, *__ = self.env[
"mail.tracking.email"
]._mailgun_values()
for partner in self:
res = requests.get(
urljoin(api_url, "/v3/%s/bounces/%s" % (domain, partner.email)),
auth=("api", api_key),
)
if res.status_code == 200 and not partner.email_bounced:
partner.email_bounced = True
elif res.status_code == 404 and partner.email_bounced:
partner.email_bounced = False
def force_set_bounced(self):
"""
Forces partner's email into Mailgun's bounces list
API documentation:
https://documentation.mailgun.com/en/latest/api-suppressions.html
"""
api_key, api_url, domain, *__ = self.env[
"mail.tracking.email"
]._mailgun_values()
for partner in self:
res = requests.post(
urljoin(api_url, "/v3/%s/bounces" % domain),
auth=("api", api_key),
data={"address": partner.email},
)
partner.email_bounced = res.status_code == 200 and not partner.email_bounced
def force_unset_bounced(self):
"""
Forces partner's email deletion from Mailgun's bounces list
API documentation:
https://documentation.mailgun.com/en/latest/api-suppressions.html
"""
api_key, api_url, domain, *__ = self.env[
"mail.tracking.email"
]._mailgun_values()
for partner in self:
res = requests.delete(
urljoin(api_url, "/v3/%s/bounces/%s" % (domain, partner.email)),
auth=("api", api_key),
)
if res.status_code in (200, 404) and partner.email_bounced:
partner.email_bounced = False
def _autocheck_partner_email(self):
for partner in self:
partner.with_context(mailgun_auto_check=True).check_email_validity()
@api.model
def create(self, vals):
if "email" in vals and self.env["ir.config_parameter"].sudo().get_param(
"mailgun.auto_check_partner_email"
):
self._autocheck_partner_email()
return super().create(vals)
def write(self, vals):
if "email" in vals and self.env["ir.config_parameter"].sudo().get_param(
"mailgun.auto_check_partner_email"
):
self._autocheck_partner_email()
return super().write(vals)