diff --git a/mail_tracking_mailgun/README.rst b/mail_tracking_mailgun/README.rst index 813210778..fb6f083fb 100644 --- a/mail_tracking_mailgun/README.rst +++ b/mail_tracking_mailgun/README.rst @@ -23,7 +23,7 @@ Mail tracking for Mailgun :target: https://runbot.odoo-community.org/runbot/205/14.0 :alt: Try me on Runbot -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| This module integrates mail_tracking events with Mailgun webhooks. @@ -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 * Carlos Roca 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 c094c6871..aebd127b5 100644 --- a/mail_tracking_mailgun/__manifest__.py +++ b/mail_tracking_mailgun/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Mail tracking for Mailgun", "summary": "Mail tracking and Mailgun webhooks integration", - "version": "14.0.1.0.0", + "version": "14.0.2.0.0", "category": "Social Network", "website": "https://github.com/OCA/social", "author": "Tecnativa, Odoo Community Association (OCA)", @@ -14,5 +14,9 @@ "application": False, "installable": True, "depends": ["mail_tracking"], - "data": ["views/res_partner.xml", "views/mail_tracking_email.xml"], + "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..d8a33106d --- /dev/null +++ b/mail_tracking_mailgun/controllers/main.py @@ -0,0 +1,75 @@ +# 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/14.0.2.0.0/post-migration.py b/mail_tracking_mailgun/migrations/14.0.2.0.0/post-migration.py new file mode 100644 index 000000000..f5ed56cd5 --- /dev/null +++ b/mail_tracking_mailgun/migrations/14.0.2.0.0/post-migration.py @@ -0,0 +1,34 @@ +# 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 auto registering. Do that process now. + """ + if version != "14.0.1.0.0": + return + 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 5d273c049..529925fb9 100644 --- a/mail_tracking_mailgun/models/mail_tracking_email.py +++ b/mail_tracking_mailgun/models/mail_tracking_email.py @@ -1,11 +1,12 @@ # 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 import logging +from collections import namedtuple from datetime import datetime +from urllib.parse import urljoin import requests @@ -15,6 +16,22 @@ from odoo.tools import email_split _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" @@ -29,49 +46,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") @@ -83,43 +81,17 @@ class MailTrackingEmail(models.Model): 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 + 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 @@ -159,20 +131,22 @@ class MailTrackingEmail(models.Model): } ) # Mapping for special events - if mailgun_event_type == "bounced": + if mailgun_event_type == "failed": + delivery_status = event.get("delivery-status", {}) metadata.update( { - "error_type": event.get("code", False), - "error_description": event.get("error", False), - "error_details": event.get("notification", False), + "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 == "dropped": + elif mailgun_event_type == "rejected": + reject = event.get("reject", {}) metadata.update( { - "error_type": event.get("reason", False), - "error_description": event.get("code", False), - "error_details": event.get("description", False), + "error_type": "rejected", + "error_description": reject.get("reason", False), + "error_details": reject.get("description", False), } ) elif mailgun_event_type == "complained": @@ -185,87 +159,79 @@ 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().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( + int(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) 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( - "{}/{}/events".format(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 f0b50cc44..5dc214644 100644 --- a/mail_tracking_mailgun/models/mail_tracking_event.py +++ b/mail_tracking_mailgun/models/mail_tracking_event.py @@ -7,7 +7,16 @@ from odoo import fields, models class MailTrackingEvent(models.Model): _inherit = "mail.tracking.event" - mailgun_id = fields.Char(string="Mailgun Event ID", copy="False", readonly=True) + _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): res = super()._process_data(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 cdccc2f96..0e6bc45a2 100644 --- a/mail_tracking_mailgun/models/res_partner.py +++ b/mail_tracking_mailgun/models/res_partner.py @@ -4,6 +4,8 @@ # 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 @@ -43,10 +45,8 @@ 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" @@ -55,9 +55,8 @@ class ResPartner(models.Model): ) 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), + urljoin(params.api_url, "/v3/address/validate"), + auth=("api", params.validation_key), params={"address": partner.email, "mailbox_verification": True}, ) if ( @@ -127,12 +126,12 @@ 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( - "{}/{}/bounces/{}".format(api_url, domain, partner.email), + urljoin(api_url, "/v3/%s/bounces/%s" % (domain, partner.email)), auth=("api", api_key), ) if res.status_code == 200 and not partner.email_bounced: @@ -146,12 +145,12 @@ 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( - "{}/{}/bounces".format(api_url, domain), + urljoin(api_url, "/v3/%s/bounces" % domain), auth=("api", api_key), data={"address": partner.email}, ) @@ -163,12 +162,12 @@ 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( - "{}/{}/bounces/{}".format(api_url, domain, partner.email), + 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: 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 31c3bd9e0..c99b3e30b 100644 --- a/mail_tracking_mailgun/readme/CONTRIBUTORS.rst +++ b/mail_tracking_mailgun/readme/CONTRIBUTORS.rst @@ -6,4 +6,5 @@ * David Vidal * Rafael Blasco * Ernesto Tejeda + * Jairo Llopis * Carlos Roca 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 571d814f0..0809ac930 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