From 0f37487265a77889022d074621a2276da89e757f Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Thu, 28 Oct 2021 11:33:59 +0100 Subject: [PATCH] [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]: https://github.com/OCA/social/blob/f73de421e28a43d018176f61725a3a59665f715d/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 --- mail_tracking_mailgun/README.rst | 46 +- mail_tracking_mailgun/__init__.py | 2 + mail_tracking_mailgun/__manifest__.py | 1 + mail_tracking_mailgun/controllers/__init__.py | 1 + mail_tracking_mailgun/controllers/main.py | 76 +++ .../migrations/12.0.2.0.0/post-migration.py | 32 ++ .../models/mail_tracking_email.py | 283 +++++------ .../models/mail_tracking_event.py | 5 + mail_tracking_mailgun/models/res_partner.py | 34 +- mail_tracking_mailgun/readme/CONFIGURE.rst | 30 +- mail_tracking_mailgun/readme/CONTRIBUTORS.rst | 1 + mail_tracking_mailgun/readme/INSTALL.rst | 6 + mail_tracking_mailgun/readme/ROADMAP.rst | 5 + .../static/description/index.html | 83 ++-- mail_tracking_mailgun/tests/test_mailgun.py | 467 ++++++++++-------- mail_tracking_mailgun/wizards/__init__.py | 1 + .../wizards/res_config_settings.py | 120 +++++ .../wizards/res_config_settings_views.xml | 81 +++ 18 files changed, 805 insertions(+), 469 deletions(-) create mode 100644 mail_tracking_mailgun/controllers/__init__.py create mode 100644 mail_tracking_mailgun/controllers/main.py create mode 100644 mail_tracking_mailgun/migrations/12.0.2.0.0/post-migration.py create mode 100644 mail_tracking_mailgun/readme/INSTALL.rst create mode 100644 mail_tracking_mailgun/wizards/__init__.py create mode 100644 mail_tracking_mailgun/wizards/res_config_settings.py create mode 100644 mail_tracking_mailgun/wizards/res_config_settings_views.xml diff --git a/mail_tracking_mailgun/README.rst b/mail_tracking_mailgun/README.rst index d3edb5741..a23737788 100644 --- a/mail_tracking_mailgun/README.rst +++ b/mail_tracking_mailgun/README.rst @@ -38,33 +38,27 @@ function used here. .. contents:: :local: +Installation +============ + +If you're using a multi-database installation (with or without dbfilter option) +where /web/databse/selector returns a list of more than one database, then +you need to add ``mail_tracking_mailgun`` addon to wide load addons list +(by default, only ``web`` addon), setting ``--load`` option. + +Example: ``--load=web,mail_tracking,mail_tracking_mailgun`` + Configuration ============= -You must configure Mailgun webhooks in order to receive mail events: +To configure this module, you need to: -1. Got a Mailgun account and validate your sending domain. -2. Go to Webhook tab and configure the below URL for each event: - -.. code:: html - - https:///mail/tracking/all/ - -Replace '' with your Odoo install domain name -and '' with your database name. - -In order to validate Mailgun webhooks you have to configure the following system -parameters: - -- `mailgun.apikey`: You can find Mailgun api_key in your validated sending - domain. -- `mailgun.api_url`: It should be fine as it is, but it could change in the - future. -- `mailgun.domain`: In case your sending domain is different from the one - configured in `mail.catchall.domain`. -- `mailgun.validation_key`: If you want to be able to check mail address - validity you must config this parameter with your account Public Validation - Key. +#. Go to Mailgun, create an account and validate your sending domain. +#. Go back to Odoo. +#. Go to *Settings > General Settings > Discuss > Enable mail tracking with Mailgun*. +#. Fill all the values. The only one required is the API key. +#. Optionally click *Unregister Mailgun webhooks* and accept. +#. Click *Register Mailgun webhooks*. You can also config partner email autocheck with this system parameter: @@ -94,6 +88,11 @@ Known issues / Roadmap * There's no support for more than one Mailgun mail server. +* Automate more webhook registration. It would be nice to not have to click the + "Unregister Mailgun webhooks" and "Register Mailgun webhooks" when setting up + Mailgun in Odoo. However, it doesn't come without its `conceptual complexities + `__. + Bug Tracker =========== @@ -123,6 +122,7 @@ Contributors * David Vidal * Rafael Blasco * Ernesto Tejeda + * Jairo Llopis Other credits ~~~~~~~~~~~~~ diff --git a/mail_tracking_mailgun/__init__.py b/mail_tracking_mailgun/__init__.py index 69f7babdf..2237bdc4c 100644 --- a/mail_tracking_mailgun/__init__.py +++ b/mail_tracking_mailgun/__init__.py @@ -1,3 +1,5 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import controllers from . import models +from . import wizards diff --git a/mail_tracking_mailgun/__manifest__.py b/mail_tracking_mailgun/__manifest__.py index bf7f93de2..6ec96d69e 100644 --- a/mail_tracking_mailgun/__manifest__.py +++ b/mail_tracking_mailgun/__manifest__.py @@ -20,5 +20,6 @@ "data": [ "views/res_partner.xml", "views/mail_tracking_email.xml", + "wizards/res_config_settings_views.xml", ] } diff --git a/mail_tracking_mailgun/controllers/__init__.py b/mail_tracking_mailgun/controllers/__init__.py new file mode 100644 index 000000000..12a7e529b --- /dev/null +++ b/mail_tracking_mailgun/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/mail_tracking_mailgun/controllers/main.py b/mail_tracking_mailgun/controllers/main.py new file mode 100644 index 000000000..25101beeb --- /dev/null +++ b/mail_tracking_mailgun/controllers/main.py @@ -0,0 +1,76 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import hashlib +import hmac +import logging + +from datetime import datetime, timedelta + +from werkzeug.exceptions import NotAcceptable + +from odoo import _ +from odoo.exceptions import ValidationError +from odoo.http import request, route + +from ...mail_tracking.controllers import main +from ...web.controllers.main import ensure_db + +_logger = logging.getLogger(__name__) + + +class MailTrackingController(main.MailTrackingController): + def _mail_tracking_mailgun_webhook_verify(self, timestamp, token, signature): + """Avoid mailgun webhook attacks. + + See https://documentation.mailgun.com/en/latest/user_manual.html#securing-webhooks + """ # noqa: E501 + # Request cannot be old + processing_time = datetime.utcnow() - datetime.utcfromtimestamp(int(timestamp)) + if not timedelta() < processing_time < timedelta(minutes=10): + raise ValidationError(_("Request is too old")) + # Avoid replay attacks + try: + processed_tokens = ( + request.env.registry._mail_tracking_mailgun_processed_tokens + ) + except AttributeError: + processed_tokens = ( + request.env.registry._mail_tracking_mailgun_processed_tokens + ) = set() + if token in processed_tokens: + raise ValidationError(_("Request was already processed")) + processed_tokens.add(token) + params = request.env["mail.tracking.email"]._mailgun_values() + # Assert signature + if not params.webhook_signing_key: + _logger.warning( + "Skipping webhook payload verification. " + "Set `mailgun.webhook_signing_key` config parameter to enable" + ) + return + hmac_digest = hmac.new( + key=params.webhook_signing_key.encode(), + msg=("{}{}".format(timestamp, token)).encode(), + digestmod=hashlib.sha256, + ).hexdigest() + if not hmac.compare_digest(str(signature), str(hmac_digest)): + raise ValidationError(_("Wrong signature")) + + @route(["/mail/tracking/mailgun/all"], auth="none", type="json", csrf=False) + def mail_tracking_mailgun_webhook(self): + """Process webhooks from Mailgun.""" + ensure_db() + # Verify and return 406 in case of failure, to avoid retries + # See https://documentation.mailgun.com/en/latest/user_manual.html#routes + try: + self._mail_tracking_mailgun_webhook_verify( + **request.jsonrequest["signature"] + ) + except ValidationError as error: + raise NotAcceptable from error + # Process event + request.env["mail.tracking.email"].sudo()._mailgun_event_process( + request.jsonrequest["event-data"], + self._request_metadata(), + ) diff --git a/mail_tracking_mailgun/migrations/12.0.2.0.0/post-migration.py b/mail_tracking_mailgun/migrations/12.0.2.0.0/post-migration.py new file mode 100644 index 000000000..7101bab7f --- /dev/null +++ b/mail_tracking_mailgun/migrations/12.0.2.0.0/post-migration.py @@ -0,0 +1,32 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from openupgradelib import openupgrade + +_logger = logging.getLogger(__name__) + + +@openupgrade.migrate() +def migrate(env, version): + """Update webhooks. + + This version dropped support for legacy webhooks and added support for + webhook autoregistering. Do that process now. + """ + settings = env["res.config.settings"].create() + if not settings.mail_tracking_mailgun_enabled: + _logger.warning("Not updating webhooks because mailgun is not configured") + return + _logger.info("Updating mailgun webhooks") + try: + settings.mail_tracking_mailgun_unregister_webhooks() + settings.mail_tracking_mailgun_register_webhooks() + except Exception: + # Don't fail the update if you can't register webhooks; it can be a + # failing network condition or air-gapped upgrade, and that's OK, you + # can just update them later + _logger.warning( + "Failed to update mailgun webhooks; do that manually", exc_info=True + ) diff --git a/mail_tracking_mailgun/models/mail_tracking_email.py b/mail_tracking_mailgun/models/mail_tracking_email.py index 77ec2db05..26815586d 100644 --- a/mail_tracking_mailgun/models/mail_tracking_email.py +++ b/mail_tracking_mailgun/models/mail_tracking_email.py @@ -1,9 +1,11 @@ # Copyright 2016 Tecnativa - Antonio Espinosa # Copyright 2017 Tecnativa - David Vidal +# Copyright 2021 Tecnativa - Jairo Llopis # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import hashlib -import hmac +from collections import namedtuple +from urllib.parse import urljoin + import requests from datetime import datetime from odoo import _, api, fields, models @@ -13,6 +15,22 @@ from odoo.tools import email_split import logging _logger = logging.getLogger(__name__) +MailgunParameters = namedtuple( + "MailgunParameters", + ( + "api_key", + "api_url", + "domain", + "validation_key", + "webhooks_domain", + "webhook_signing_key", + ), +) + + +class EventNotFoundWarning(Warning): + pass + class MailTrackingEmail(models.Model): _inherit = "mail.tracking.email" @@ -27,43 +45,30 @@ class MailTrackingEmail(models.Model): return country.id return False - @property - def _mailgun_mandatory_fields(self): - return ('event', 'timestamp', 'token', 'signature', - 'tracking_email_id', 'odoo_db') + @api.model + def _mailgun_event2type(self, event, default="UNKNOWN"): + """Return the ``mail.tracking.event`` equivalent event - @property - def _mailgun_event_type_mapping(self): - return { - # Mailgun event type: tracking event type + Args: + event: Mailgun event response from API. + default: Value to return when not found. + """ + # Mailgun event type: tracking event type + equivalents = { 'delivered': 'delivered', 'opened': 'open', 'clicked': 'click', 'unsubscribed': 'unsub', 'complained': 'spam', - 'bounced': 'hard_bounce', - 'dropped': 'reject', 'accepted': 'sent', - 'failed': 'error', - 'rejected': 'error', + "failed": ( + "hard_bounce" if event.get("severity") == "permanent" else "soft_bounce" + ), + "rejected": "reject", } + return equivalents.get(event.get("event"), default) - def _mailgun_event_type_verify(self, event): - event = event or {} - mailgun_event_type = event.get('event') - if mailgun_event_type not in self._mailgun_event_type_mapping: - _logger.error("Mailgun: event type '%s' not supported", - mailgun_event_type) - return False - # OK, event type is valid - return True - - def _mailgun_signature(self, api_key, timestamp, token): - return hmac.new( - key=bytes(api_key, 'utf-8'), - msg=bytes('{}{}'.format(str(timestamp), str(token)), 'utf-8'), - digestmod=hashlib.sha256).hexdigest() - + @api.model def _mailgun_values(self): icp = self.env['ir.config_parameter'].sudo() api_key = icp.get_param('mailgun.apikey') @@ -74,43 +79,19 @@ class MailTrackingEmail(models.Model): catchall_domain = icp.get_param('mail.catchall.domain') domain = icp.get_param('mailgun.domain', catchall_domain) if not domain: - raise ValidationError(_('A Mailgun domain value is needed!')) - validation_key = icp.get_param('mailgun.validation_key') - return api_key, api_url, domain, validation_key - - def _mailgun_signature_verify(self, event): - event = event or {} - icp = self.env['ir.config_parameter'].sudo() - api_key = icp.get_param('mailgun.apikey') - if not api_key: - _logger.warning("No Mailgun api key configured. " - "Please add 'mailgun.apikey' to System parameters " - "to enable Mailgun authentication webhoook " - "requests. More info at: " - "https://documentation.mailgun.com/" - "user_manual.html#webhooks") - else: - timestamp = event.get('timestamp') - token = event.get('token') - signature = event.get('signature') - event_digest = self._mailgun_signature(api_key, timestamp, token) - if signature != event_digest: - _logger.error("Mailgun: Invalid signature '%s' != '%s'", - signature, event_digest) - return False - # OK, signature is valid - return True - - def _db_verify(self, event): - event = event or {} - odoo_db = event.get('odoo_db') - current_db = self.env.cr.dbname - if odoo_db != current_db: - _logger.error("Mailgun: Database '%s' is not the current database", - odoo_db) - return False - # OK, DB is current - return True + raise ValidationError(_("A Mailgun domain value is needed!")) + validation_key = icp.get_param("mailgun.validation_key") + web_base_url = icp.get_param("web.base.url") + webhooks_domain = icp.get_param("mailgun.webhooks_domain", web_base_url) + webhook_signing_key = icp.get_param("mailgun.webhook_signing_key") + return MailgunParameters( + api_key, + api_url, + domain, + validation_key, + webhooks_domain, + webhook_signing_key, + ) def _mailgun_metadata(self, mailgun_event_type, event, metadata): # Get Mailgun timestamp when found @@ -147,18 +128,24 @@ class MailTrackingEmail(models.Model): event.get('country', False)), }) # Mapping for special events - if mailgun_event_type == 'bounced': - metadata.update({ - 'error_type': event.get('code', False), - 'error_description': event.get('error', False), - 'error_details': event.get('notification', False), - }) - elif mailgun_event_type == 'dropped': - metadata.update({ - 'error_type': event.get('reason', False), - 'error_description': event.get('code', False), - 'error_details': event.get('description', False), - }) + if mailgun_event_type == "failed": + delivery_status = event.get("delivery-status", {}) + metadata.update( + { + "error_type": delivery_status.get("code", False), + "error_description": delivery_status.get("message", False), + "error_details": delivery_status.get("description", False), + } + ) + elif mailgun_event_type == "rejected": + reject = event.get("reject", {}) + metadata.update( + { + "error_type": "rejected", + "error_description": reject.get("reason", False), + "error_details": reject.get("description", False), + } + ) elif mailgun_event_type == 'complained': metadata.update({ 'error_type': 'spam', @@ -168,91 +155,77 @@ class MailTrackingEmail(models.Model): }) return metadata - def _mailgun_tracking_get(self, event): - tracking = False - tracking_email_id = event.get('tracking_email_id', False) - if tracking_email_id and tracking_email_id.isdigit(): - tracking = self.search([('id', '=', tracking_email_id)], limit=1) - return tracking - - def _event_is_from_mailgun(self, event): - event = event or {} - return all([k in event for k in self._mailgun_mandatory_fields]) - @api.model - def event_process(self, request, post, metadata, event_type=None): - res = super(MailTrackingEmail, self).event_process( - request, post, metadata, event_type=event_type) - if res == 'NONE' and self._event_is_from_mailgun(post): - if not self._mailgun_signature_verify(post): - res = 'ERROR: Signature' - elif not self._mailgun_event_type_verify(post): - res = 'ERROR: Event type not supported' - elif not self._db_verify(post): - res = 'ERROR: Invalid DB' - else: - res = 'OK' - if res == 'OK': - mailgun_event_type = post.get('event') - mapped_event_type = self._mailgun_event_type_mapping.get( - mailgun_event_type) or event_type - if not mapped_event_type: # pragma: no cover - res = 'ERROR: Bad event' - tracking = self._mailgun_tracking_get(post) - if not tracking: - res = 'ERROR: Tracking not found' - if res == 'OK': - # Complete metadata with mailgun event info - metadata = self._mailgun_metadata( - mailgun_event_type, post, metadata) - # Create event - tracking.event_create(mapped_event_type, metadata) - if res != 'NONE': - if event_type: - _logger.info( - "Mailgun: event '%s' process '%s'", event_type, res) - else: - _logger.info("Mailgun: event process '%s'", res) - return res + def _mailgun_event_process(self, event_data, metadata): + """Retrieve (and maybe create) mailgun event from API data payload. + + In https://documentation.mailgun.com/en/latest/api-events.html#event-structure + you can read the event payload format as obtained from webhooks or calls to API. + """ + if event_data['user-variables']['odoo_db'] != self.env.cr.dbname: + raise ValidationError(_("Wrong database for event!")) + # Do nothing if event was already processed + mailgun_id = event_data["id"] + db_event = self.env["mail.tracking.event"].search( + [("mailgun_id", "=", mailgun_id)], limit=1 + ) + if db_event: + _logger.debug("Mailgun event already found in DB: %s", mailgun_id) + return db_event + # Do nothing if tracking email for event is not found + message_id = event_data["message"]["headers"]["message-id"] + recipient = event_data["recipient"] + tracking_email = self.browse(event_data['user-variables']['tracking_email_id']) + mailgun_event_type = event_data["event"] + # Process event + state = self._mailgun_event2type(event_data, mailgun_event_type) + metadata = self._mailgun_metadata(mailgun_event_type, event_data, metadata) + _logger.info( + "Importing mailgun event %s (%s message %s for %s)", + mailgun_id, + mailgun_event_type, + message_id, + recipient, + ) + tracking_email.event_create(state, metadata) @api.multi def action_manual_check_mailgun(self): - """ - Manual check against Mailgun API + """Manual check against Mailgun API + API Documentation: https://documentation.mailgun.com/en/latest/api-events.html """ - api_key, api_url, domain, validation_key = self._mailgun_values() + api_key, api_url, domain, *__ = self._mailgun_values() for tracking in self: if not tracking.mail_message_id: raise UserError(_('There is no tracked message!')) message_id = tracking.mail_message_id.message_id.replace( "<", "").replace(">", "") - res = requests.get( - '%s/%s/events' % (api_url, domain), - auth=("api", api_key), - params={ - "begin": tracking.timestamp, - "ascending": "yes", - "message-id": message_id, - } - ) - if not res or res.status_code != 200: - raise ValidationError(_( - "Couldn't retrieve Mailgun information")) - content = res.json() - if "items" not in content: - raise ValidationError(_("Event information not longer stored")) - for item in content["items"]: - # mailgun event hasn't been synced and recipient is the same as - # in the evaluated tracking. We use email_split since tracking - # recipient could come in format: "example" - if not self.env['mail.tracking.event'].search( - [('mailgun_id', '=', item["id"])]) and ( - item.get("recipient", "") == - email_split(tracking.recipient)[0]): - mapped_event_type = self._mailgun_event_type_mapping.get( - item["event"], item["event"]) - metadata = self._mailgun_metadata( - mapped_event_type, item, {}) - tracking.event_create(mapped_event_type, metadata) + events = [] + url = urljoin(api_url, "/v3/%s/events" % domain) + params = { + "begin": tracking.timestamp, + "ascending": "yes", + "message-id": message_id, + "recipient": email_split(tracking.recipient)[0], + } + while url: + res = requests.get( + url, + auth=("api", api_key), + params=params, + ) + if not res or res.status_code != 200: + raise UserError(_("Couldn't retrieve Mailgun information")) + iter_events = res.json().get("items", []) + if not iter_events: + # Loop no more + break + events.extend(iter_events) + # Loop over pagination + url = res.json().get("paging", {}).get("next") + if not events: + raise UserError(_("Event information not longer stored")) + for event in events: + self.sudo()._mailgun_event_process(event, {}) diff --git a/mail_tracking_mailgun/models/mail_tracking_event.py b/mail_tracking_mailgun/models/mail_tracking_event.py index 201b4ac4e..d08c9ccaf 100644 --- a/mail_tracking_mailgun/models/mail_tracking_event.py +++ b/mail_tracking_mailgun/models/mail_tracking_event.py @@ -7,10 +7,15 @@ from odoo import models, fields class MailTrackingEvent(models.Model): _inherit = "mail.tracking.event" + _sql_constraints = [ + ("mailgun_id_unique", "UNIQUE(mailgun_id)", "Mailgun event IDs must be unique!") + ] + mailgun_id = fields.Char( string="Mailgun Event ID", copy="False", readonly=True, + index=True, ) def _process_data(self, tracking_email, metadata, event_type, state): diff --git a/mail_tracking_mailgun/models/res_partner.py b/mail_tracking_mailgun/models/res_partner.py index 45af140dc..0ad562a80 100644 --- a/mail_tracking_mailgun/models/res_partner.py +++ b/mail_tracking_mailgun/models/res_partner.py @@ -4,8 +4,9 @@ # Copyright 2017 Tecnativa - David Vidal # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -import requests +from urllib.parse import urljoin +import requests from odoo import _, api, models from odoo.exceptions import UserError @@ -42,16 +43,15 @@ class ResPartner(models.Model): API documentation: https://documentation.mailgun.com/en/latest/api-email-validation.html """ - api_key, api_url, domain, validation_key = self.env[ - 'mail.tracking.email']._mailgun_values() - if not validation_key: + 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( - # Validation API url is always the same - 'https://api.mailgun.net/v3/address/validate', - auth=("api", validation_key), params={ + urljoin(params.api_url, "/v3/address/validate"), + auth=("api", params.validation_key), + params={ "address": partner.email, "mailbox_verification": True, }) @@ -89,7 +89,7 @@ class ResPartner(models.Model): 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" + _("%s couldn't be verified. Either the request couldn't" " be completed or the mailbox provider doesn't " "support email verification") % (partner.email)) @@ -100,12 +100,13 @@ class ResPartner(models.Model): API documentation: https://documentation.mailgun.com/en/latest/api-suppressions.html """ - api_key, api_url, domain, validation_key = self.env[ + api_key, api_url, domain, *__ = self.env[ 'mail.tracking.email']._mailgun_values() for partner in self: res = requests.get( - '%s/%s/bounces/%s' % (api_url, domain, partner.email), - auth=("api", api_key)) + 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: @@ -118,11 +119,11 @@ class ResPartner(models.Model): API documentation: https://documentation.mailgun.com/en/latest/api-suppressions.html """ - api_key, api_url, domain, validation_key = self.env[ + api_key, api_url, domain, *__ = self.env[ 'mail.tracking.email']._mailgun_values() for partner in self: res = requests.post( - '%s/%s/bounces' % (api_url, domain), + urljoin(api_url, "/v3/%s/bounces" % domain), auth=("api", api_key), data={'address': partner.email}) partner.email_bounced = ( @@ -135,12 +136,13 @@ class ResPartner(models.Model): API documentation: https://documentation.mailgun.com/en/latest/api-suppressions.html """ - api_key, api_url, domain, validation_key = self.env[ + api_key, api_url, domain, *__ = self.env[ 'mail.tracking.email']._mailgun_values() for partner in self: res = requests.delete( - '%s/%s/bounces/%s' % (api_url, domain, partner.email), - auth=("api", api_key)) + 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 diff --git a/mail_tracking_mailgun/readme/CONFIGURE.rst b/mail_tracking_mailgun/readme/CONFIGURE.rst index 59ade2d2a..cb85e3032 100644 --- a/mail_tracking_mailgun/readme/CONFIGURE.rst +++ b/mail_tracking_mailgun/readme/CONFIGURE.rst @@ -1,27 +1,11 @@ -You must configure Mailgun webhooks in order to receive mail events: +To configure this module, you need to: -1. Got a Mailgun account and validate your sending domain. -2. Go to Webhook tab and configure the below URL for each event: - -.. code:: html - - https:///mail/tracking/all/ - -Replace '' with your Odoo install domain name -and '' with your database name. - -In order to validate Mailgun webhooks you have to configure the following system -parameters: - -- `mailgun.apikey`: You can find Mailgun api_key in your validated sending - domain. -- `mailgun.api_url`: It should be fine as it is, but it could change in the - future. -- `mailgun.domain`: In case your sending domain is different from the one - configured in `mail.catchall.domain`. -- `mailgun.validation_key`: If you want to be able to check mail address - validity you must config this parameter with your account Public Validation - Key. +#. Go to Mailgun, create an account and validate your sending domain. +#. Go back to Odoo. +#. Go to *Settings > General Settings > Discuss > Enable mail tracking with Mailgun*. +#. Fill all the values. The only one required is the API key. +#. Optionally click *Unregister Mailgun webhooks* and accept. +#. Click *Register Mailgun webhooks*. You can also config partner email autocheck with this system parameter: diff --git a/mail_tracking_mailgun/readme/CONTRIBUTORS.rst b/mail_tracking_mailgun/readme/CONTRIBUTORS.rst index 9e21d5b16..0f066b709 100644 --- a/mail_tracking_mailgun/readme/CONTRIBUTORS.rst +++ b/mail_tracking_mailgun/readme/CONTRIBUTORS.rst @@ -6,3 +6,4 @@ * David Vidal * Rafael Blasco * Ernesto Tejeda + * Jairo Llopis diff --git a/mail_tracking_mailgun/readme/INSTALL.rst b/mail_tracking_mailgun/readme/INSTALL.rst new file mode 100644 index 000000000..a755874b6 --- /dev/null +++ b/mail_tracking_mailgun/readme/INSTALL.rst @@ -0,0 +1,6 @@ +If you're using a multi-database installation (with or without dbfilter option) +where /web/databse/selector returns a list of more than one database, then +you need to add ``mail_tracking_mailgun`` addon to wide load addons list +(by default, only ``web`` addon), setting ``--load`` option. + +Example: ``--load=web,mail_tracking,mail_tracking_mailgun`` diff --git a/mail_tracking_mailgun/readme/ROADMAP.rst b/mail_tracking_mailgun/readme/ROADMAP.rst index 155380202..a3cd6ca86 100644 --- a/mail_tracking_mailgun/readme/ROADMAP.rst +++ b/mail_tracking_mailgun/readme/ROADMAP.rst @@ -1 +1,6 @@ * There's no support for more than one Mailgun mail server. + +* Automate more webhook registration. It would be nice to not have to click the + "Unregister Mailgun webhooks" and "Register Mailgun webhooks" when setting up + Mailgun in Odoo. However, it doesn't come without its `conceptual complexities + `__. diff --git a/mail_tracking_mailgun/static/description/index.html b/mail_tracking_mailgun/static/description/index.html index 3564578c4..a97d9f843 100644 --- a/mail_tracking_mailgun/static/description/index.html +++ b/mail_tracking_mailgun/static/description/index.html @@ -3,7 +3,7 @@ - + Mail tracking for Mailgun