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