[MIG] mail_tracking_mailgun: Migration to 11.0

This commit is contained in:
David 2018-05-08 18:24:36 +02:00 committed by nicolas
parent 02d0c30d3c
commit ccee1257d8
9 changed files with 87 additions and 97 deletions

View File

@ -47,11 +47,12 @@ You can also config partner email autocheck with this system parameter:
Usage Usage
===== =====
In your mail tracking status screens (explained on module *mail_tracking*), you will In your mail tracking status screens (explained on module *mail_tracking*), you
see a more accurate information, like the 'Received' or 'Bounced' status, which are will see a more accurate information, like the 'Received' or 'Bounced' status,
not usually detected by normal SMTP servers. which are not usually detected by normal SMTP servers.
It's also possible to make some checks to the partner's email addresses against the Mailgun API: It's also possible to make some checks to the partner's email addresses against
the Mailgun API:
- Check if the partner's email is in Mailgun's bounced list. - Check if the partner's email is in Mailgun's bounced list.
- Check the validity of the partner's mailbox. - Check the validity of the partner's mailbox.
@ -64,7 +65,7 @@ short lifespan, so after 24h they won't be recoverable.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot :alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/205/9.0 :target: https://runbot.odoo-community.org/runbot/205/11.0
Known issues / Roadmap Known issues / Roadmap
====================== ======================

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models from . import models

View File

@ -1,15 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Tecnativa - Antonio Espinosa # Copyright 2016 Tecnativa - Antonio Espinosa
# Copyright 2016 Tecnativa - Carlos Dauden # Copyright 2016 Tecnativa - Carlos Dauden
# Copyright 2017 Tecnativa - Pedro M. Baeza # Copyright 2017 Tecnativa - Pedro M. Baeza
# Copyright 2017 Tecnativa - David Vidal # Copyright 2017-18 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{ {
"name": "Mail tracking for Mailgun", "name": "Mail tracking for Mailgun",
"summary": "Mail tracking and Mailgun webhooks integration", "summary": "Mail tracking and Mailgun webhooks integration",
"version": "10.0.1.1.3", "version": "11.0.1.0.2",
"category": "Social Network", "category": "Social Network",
"website": "https://odoo-community.org/", "website": "https://github.com/OCA/social",
"author": "Tecnativa, " "author": "Tecnativa, "
"Odoo Community Association (OCA)", "Odoo Community Association (OCA)",
"license": "AGPL-3", "license": "AGPL-3",

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import ir_mail_server from . import ir_mail_server

View File

@ -5,7 +5,6 @@
import hashlib import hashlib
import hmac import hmac
import json
import requests import requests
from datetime import datetime from datetime import datetime
from odoo import _, api, fields, models from odoo import _, api, fields, models
@ -60,8 +59,8 @@ class MailTrackingEmail(models.Model):
def _mailgun_signature(self, api_key, timestamp, token): def _mailgun_signature(self, api_key, timestamp, token):
return hmac.new( return hmac.new(
key=str(api_key), key=bytes(api_key, 'utf-8'),
msg='{}{}'.format(str(timestamp), str(token)), msg=bytes('{}{}'.format(str(timestamp), str(token)), 'utf-8'),
digestmod=hashlib.sha256).hexdigest() digestmod=hashlib.sha256).hexdigest()
def _mailgun_values(self): def _mailgun_values(self):
@ -115,7 +114,7 @@ class MailTrackingEmail(models.Model):
ts = event.get('timestamp', False) ts = event.get('timestamp', False)
try: try:
ts = float(ts) ts = float(ts)
except: except Exception:
ts = False ts = False
if ts: if ts:
dt = datetime.utcfromtimestamp(ts) dt = datetime.utcfromtimestamp(ts)
@ -135,7 +134,7 @@ class MailTrackingEmail(models.Model):
'ua_type': 'client-type', 'ua_type': 'client-type',
'url': 'url', 'url': 'url',
} }
for k, v in mapping.iteritems(): for k, v in mapping.items():
if event.get(v, False): if event.get(v, False):
metadata[k] = event[v] metadata[k] = event[v]
# Special field mapping # Special field mapping
@ -238,7 +237,7 @@ class MailTrackingEmail(models.Model):
if not res or res.status_code != 200: if not res or res.status_code != 200:
raise ValidationError(_( raise ValidationError(_(
"Couldn't retrieve Mailgun information")) "Couldn't retrieve Mailgun information"))
content = json.loads(res.content, res.apparent_encoding) content = res.json()
if "items" not in content: if "items" not in content:
raise ValidationError(_("Event information not longer stored")) raise ValidationError(_("Event information not longer stored"))
for item in content["items"]: for item in content["items"]:

View File

@ -6,7 +6,6 @@
# 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 requests import requests
import json
from odoo import _, api, models from odoo import _, api, models
from odoo.exceptions import UserError from odoo.exceptions import UserError
@ -57,7 +56,7 @@ class ResPartner(models.Model):
raise UserError(_( raise UserError(_(
'Error %s trying to ' 'Error %s trying to '
'check mail' % res.status_code or 'of connection')) 'check mail' % res.status_code or 'of connection'))
content = json.loads(res.content, res.apparent_encoding) content = res.json()
if 'mailbox_verification' not in content: if 'mailbox_verification' not in content:
if not self.env.context.get('mailgun_auto_check'): if not self.env.context.get('mailgun_auto_check'):
raise UserError( raise UserError(

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_mailgun from . import test_mailgun

View File

@ -7,7 +7,6 @@ from odoo.tools import mute_logger
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError, ValidationError from odoo.exceptions import UserError, ValidationError
import mock import mock
import json
_packagepath = 'odoo.addons.mail_tracking_mailgun' _packagepath = 'odoo.addons.mail_tracking_mailgun'
@ -29,12 +28,12 @@ class TestMailgun(TransactionCase):
def setUp(self): def setUp(self):
super(TestMailgun, self).setUp() super(TestMailgun, self).setUp()
self.recipient = u'to@example.com' self.recipient = 'to@example.com'
self.mail, self.tracking_email = self.mail_send() self.mail, self.tracking_email = self.mail_send()
self.api_key = u'key-12345678901234567890123456789012' self.api_key = 'key-12345678901234567890123456789012'
self.domain = u'example.com' self.domain = 'example.com'
self.token = u'f1349299097a51b9a7d886fcb5c2735b426ba200ada6e9e149' self.token = 'f1349299097a51b9a7d886fcb5c2735b426ba200ada6e9e149'
self.timestamp = u'1471021089' self.timestamp = '1471021089'
self.signature = ('4fb6d4dbbe10ce5d620265dcd7a3c0b8' self.signature = ('4fb6d4dbbe10ce5d620265dcd7a3c0b8'
'ca0dede1433103891bc1ae4086e9d5b2') 'ca0dede1433103891bc1ae4086e9d5b2')
self.env['ir.config_parameter'].set_param( self.env['ir.config_parameter'].set_param(
@ -46,17 +45,17 @@ class TestMailgun(TransactionCase):
self.env['ir.config_parameter'].set_param( self.env['ir.config_parameter'].set_param(
'mailgun.auto_check_partner_email', '') 'mailgun.auto_check_partner_email', '')
self.event = { self.event = {
'Message-Id': u'<xxx.xxx.xxx-openerp-xxx-res.partner@test_db>', 'Message-Id': '<xxx.xxx.xxx-openerp-xxx-res.partner@test_db>',
'X-Mailgun-Sid': u'WyIwNjgxZSIsICJ0b0BleGFtcGxlLmNvbSIsICI3MG' 'X-Mailgun-Sid': 'WyIwNjgxZSIsICJ0b0BleGFtcGxlLmNvbSIsICI3MG'
'I0MWYiXQ==', 'I0MWYiXQ==',
'token': self.token, 'token': self.token,
'timestamp': self.timestamp, 'timestamp': self.timestamp,
'signature': self.signature, 'signature': self.signature,
'domain': u'example.com', 'domain': 'example.com',
'message-headers': u'[]', 'message-headers': '[]',
'recipient': self.recipient, 'recipient': self.recipient,
'odoo_db': self.env.cr.dbname, 'odoo_db': self.env.cr.dbname,
'tracking_email_id': u'%s' % self.tracking_email.id 'tracking_email_id': '%s' % self.tracking_email.id
} }
self.metadata = { self.metadata = {
'ip': '127.0.0.1', 'ip': '127.0.0.1',
@ -110,8 +109,8 @@ class TestMailgun(TransactionCase):
'.mail_tracking_email') '.mail_tracking_email')
def test_bad_signature(self): def test_bad_signature(self):
self.event.update({ self.event.update({
'event': u'delivered', 'event': 'delivered',
'signature': u'bad_signature', 'signature': 'bad_signature',
}) })
response = self.env['mail.tracking.email'].event_process( response = self.env['mail.tracking.email'].event_process(
None, self.event, self.metadata) None, self.event, self.metadata)
@ -121,7 +120,7 @@ class TestMailgun(TransactionCase):
'.mail_tracking_email') '.mail_tracking_email')
def test_bad_event_type(self): def test_bad_event_type(self):
self.event.update({ self.event.update({
'event': u'bad_event', 'event': 'bad_event',
}) })
response = self.env['mail.tracking.email'].event_process( response = self.env['mail.tracking.email'].event_process(
None, self.event, self.metadata) None, self.event, self.metadata)
@ -131,19 +130,19 @@ class TestMailgun(TransactionCase):
'.mail_tracking_email') '.mail_tracking_email')
def test_bad_db(self): def test_bad_db(self):
self.event.update({ self.event.update({
'event': u'delivered', 'event': 'delivered',
'odoo_db': u'bad_db', 'odoo_db': 'bad_db',
}) })
response = self.env['mail.tracking.email'].event_process( response = self.env['mail.tracking.email'].event_process(
None, self.event, self.metadata) None, self.event, self.metadata)
self.assertEqual('ERROR: Invalid DB', response) self.assertEqual('ERROR: Invalid DB', response)
def test_bad_ts(self): def test_bad_ts(self):
timestamp = u'7a' # Now time will be used instead timestamp = '7a' # Now time will be used instead
signature = ('06cc05680f6e8110e59b41152b2d1c0f' signature = ('06cc05680f6e8110e59b41152b2d1c0f'
'1045d755ef2880ff922344325c89a6d4') '1045d755ef2880ff922344325c89a6d4')
self.event.update({ self.event.update({
'event': u'delivered', 'event': 'delivered',
'timestamp': timestamp, 'timestamp': timestamp,
'signature': signature, 'signature': signature,
}) })
@ -155,8 +154,8 @@ class TestMailgun(TransactionCase):
'.mail_tracking_email') '.mail_tracking_email')
def test_tracking_not_found(self): def test_tracking_not_found(self):
self.event.update({ self.event.update({
'event': u'delivered', 'event': 'delivered',
'tracking_email_id': u'bad_id', 'tracking_email_id': 'bad_id',
}) })
response = self.env['mail.tracking.email'].event_process( response = self.env['mail.tracking.email'].event_process(
None, self.event, self.metadata) None, self.event, self.metadata)
@ -165,7 +164,7 @@ class TestMailgun(TransactionCase):
# https://documentation.mailgun.com/user_manual.html#tracking-deliveries # https://documentation.mailgun.com/user_manual.html#tracking-deliveries
def test_event_delivered(self): def test_event_delivered(self):
self.event.update({ self.event.update({
'event': u'delivered', 'event': 'delivered',
}) })
response = self.env['mail.tracking.email'].event_process( response = self.env['mail.tracking.email'].event_process(
None, self.event, self.metadata) None, self.event, self.metadata)
@ -176,20 +175,20 @@ class TestMailgun(TransactionCase):
# https://documentation.mailgun.com/user_manual.html#tracking-opens # https://documentation.mailgun.com/user_manual.html#tracking-opens
def test_event_opened(self): def test_event_opened(self):
ip = u'127.0.0.1' ip = '127.0.0.1'
user_agent = u'Odoo Test/8.0 Gecko Firefox/11.0' user_agent = 'Odoo Test/8.0 Gecko Firefox/11.0'
os_family = u'Linux' os_family = 'Linux'
ua_family = u'Firefox' ua_family = 'Firefox'
ua_type = u'browser' ua_type = 'browser'
self.event.update({ self.event.update({
'event': u'opened', 'event': 'opened',
'city': u'Mountain View', 'city': 'Mountain View',
'country': u'US', 'country': 'US',
'region': u'CA', 'region': 'CA',
'client-name': ua_family, 'client-name': ua_family,
'client-os': os_family, 'client-os': os_family,
'client-type': ua_type, 'client-type': ua_type,
'device-type': u'desktop', 'device-type': 'desktop',
'ip': ip, 'ip': ip,
'user-agent': user_agent, 'user-agent': user_agent,
}) })
@ -209,21 +208,21 @@ class TestMailgun(TransactionCase):
# https://documentation.mailgun.com/user_manual.html#tracking-clicks # https://documentation.mailgun.com/user_manual.html#tracking-clicks
def test_event_clicked(self): def test_event_clicked(self):
ip = u'127.0.0.1' ip = '127.0.0.1'
user_agent = u'Odoo Test/8.0 Gecko Firefox/11.0' user_agent = 'Odoo Test/8.0 Gecko Firefox/11.0'
os_family = u'Linux' os_family = 'Linux'
ua_family = u'Firefox' ua_family = 'Firefox'
ua_type = u'browser' ua_type = 'browser'
url = u'https://odoo-community.org' url = 'https://odoo-community.org'
self.event.update({ self.event.update({
'event': u'clicked', 'event': 'clicked',
'city': u'Mountain View', 'city': 'Mountain View',
'country': u'US', 'country': 'US',
'region': u'CA', 'region': 'CA',
'client-name': ua_family, 'client-name': ua_family,
'client-os': os_family, 'client-os': os_family,
'client-type': ua_type, 'client-type': ua_type,
'device-type': u'tablet', 'device-type': 'tablet',
'ip': ip, 'ip': ip,
'user-agent': user_agent, 'user-agent': user_agent,
'url': url, 'url': url,
@ -244,20 +243,20 @@ class TestMailgun(TransactionCase):
# https://documentation.mailgun.com/user_manual.html#tracking-unsubscribes # https://documentation.mailgun.com/user_manual.html#tracking-unsubscribes
def test_event_unsubscribed(self): def test_event_unsubscribed(self):
ip = u'127.0.0.1' ip = '127.0.0.1'
user_agent = u'Odoo Test/8.0 Gecko Firefox/11.0' user_agent = 'Odoo Test/8.0 Gecko Firefox/11.0'
os_family = u'Linux' os_family = 'Linux'
ua_family = u'Firefox' ua_family = 'Firefox'
ua_type = u'browser' ua_type = 'browser'
self.event.update({ self.event.update({
'event': u'unsubscribed', 'event': 'unsubscribed',
'city': u'Mountain View', 'city': 'Mountain View',
'country': u'US', 'country': 'US',
'region': u'CA', 'region': 'CA',
'client-name': ua_family, 'client-name': ua_family,
'client-os': os_family, 'client-os': os_family,
'client-type': ua_type, 'client-type': ua_type,
'device-type': u'mobile', 'device-type': 'mobile',
'ip': ip, 'ip': ip,
'user-agent': user_agent, 'user-agent': user_agent,
}) })
@ -278,7 +277,7 @@ class TestMailgun(TransactionCase):
# user_manual.html#tracking-spam-complaints # user_manual.html#tracking-spam-complaints
def test_event_complained(self): def test_event_complained(self):
self.event.update({ self.event.update({
'event': u'complained', 'event': 'complained',
}) })
response = self.env['mail.tracking.email'].event_process( response = self.env['mail.tracking.email'].event_process(
None, self.event, self.metadata) None, self.event, self.metadata)
@ -290,12 +289,12 @@ class TestMailgun(TransactionCase):
# https://documentation.mailgun.com/user_manual.html#tracking-bounces # https://documentation.mailgun.com/user_manual.html#tracking-bounces
def test_event_bounced(self): def test_event_bounced(self):
code = u'550' code = '550'
error = (u"5.1.1 The email account does not exist.\n" error = ("5.1.1 The email account does not exist.\n"
"5.1.1 double-checking the recipient's email address") "5.1.1 double-checking the recipient's email address")
notification = u"Please, check recipient's email address" notification = "Please, check recipient's email address"
self.event.update({ self.event.update({
'event': u'bounced', 'event': 'bounced',
'code': code, 'code': code,
'error': error, 'error': error,
'notification': notification, 'notification': notification,
@ -312,11 +311,11 @@ class TestMailgun(TransactionCase):
# https://documentation.mailgun.com/user_manual.html#tracking-failures # https://documentation.mailgun.com/user_manual.html#tracking-failures
def test_event_dropped(self): def test_event_dropped(self):
reason = u'hardfail' reason = 'hardfail'
code = u'605' code = '605'
description = u'Not delivering to previously bounced address' description = 'Not delivering to previously bounced address'
self.event.update({ self.event.update({
'event': u'dropped', 'event': 'dropped',
'reason': reason, 'reason': reason,
'code': code, 'code': code,
'description': description, 'description': description,
@ -336,10 +335,10 @@ class TestMailgun(TransactionCase):
self.partner.email_bounced = False self.partner.email_bounced = False
mock_request.get.return_value.apparent_encoding = 'ascii' mock_request.get.return_value.apparent_encoding = 'ascii'
mock_request.get.return_value.status_code = 200 mock_request.get.return_value.status_code = 200
mock_request.get.return_value.content = json.dumps({ mock_request.get.return_value.json.return_value = {
'is_valid': True, 'is_valid': True,
'mailbox_verification': 'true', 'mailbox_verification': 'true',
}, ensure_ascii=True) }
# Trigger email auto validation in partner # Trigger email auto validation in partner
self.env['ir.config_parameter'].set_param( self.env['ir.config_parameter'].set_param(
'mailgun.auto_check_partner_email', 'True') 'mailgun.auto_check_partner_email', 'True')
@ -347,24 +346,24 @@ class TestMailgun(TransactionCase):
self.assertFalse(self.partner.email_bounced) self.assertFalse(self.partner.email_bounced)
self.partner.email = 'xoxoxoxo@tecnativa.com' self.partner.email = 'xoxoxoxo@tecnativa.com'
# Not a valid mailbox # Not a valid mailbox
mock_request.get.return_value.content = json.dumps({ mock_request.get.return_value.json.return_value = {
'is_valid': True, 'is_valid': True,
'mailbox_verification': 'false', 'mailbox_verification': 'false',
}, ensure_ascii=True) }
with self.assertRaises(UserError): with self.assertRaises(UserError):
self.partner.check_email_validity() self.partner.check_email_validity()
# Not a valid mail address # Not a valid mail address
mock_request.get.return_value.content = json.dumps({ mock_request.get.return_value.json.return_value = {
'is_valid': False, 'is_valid': False,
'mailbox_verification': 'false', 'mailbox_verification': 'false',
}, ensure_ascii=True) }
with self.assertRaises(UserError): with self.assertRaises(UserError):
self.partner.check_email_validity() self.partner.check_email_validity()
# Unable to fully validate # Unable to fully validate
mock_request.get.return_value.content = json.dumps({ mock_request.get.return_value.json.return_value = {
'is_valid': True, 'is_valid': True,
'mailbox_verification': 'unknown', 'mailbox_verification': 'unknown',
}, ensure_ascii=True) }
with self.assertRaises(UserError): with self.assertRaises(UserError):
self.partner.check_email_validity() self.partner.check_email_validity()
self.assertTrue(self.partner.email_bounced) self.assertTrue(self.partner.email_bounced)
@ -402,9 +401,7 @@ class TestMailgun(TransactionCase):
@mock.patch(_packagepath + '.models.mail_tracking_email.requests') @mock.patch(_packagepath + '.models.mail_tracking_email.requests')
def test_manual_check(self, mock_request): def test_manual_check(self, mock_request):
mock_request.get.return_value.content = json.dumps(self.response, mock_request.get.return_value.json.return_value = self.response
ensure_ascii=True)
mock_request.get.return_value.apparent_encoding = 'ascii'
mock_request.get.return_value.status_code = 200 mock_request.get.return_value.status_code = 200
self.tracking_email.action_manual_check_mailgun() self.tracking_email.action_manual_check_mailgun()
event = self.env['mail.tracking.event'].search( event = self.env['mail.tracking.event'].search(
@ -417,8 +414,6 @@ class TestMailgun(TransactionCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.tracking_email.action_manual_check_mailgun() self.tracking_email.action_manual_check_mailgun()
mock_request.get.return_value.status_code = 200 mock_request.get.return_value.status_code = 200
mock_request.get.return_value.content = json.dumps('{}', mock_request.get.return_value.json.return_value = {}
ensure_ascii=True)
mock_request.get.return_value.apparent_encoding = 'ascii'
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.tracking_email.action_manual_check_mailgun() self.tracking_email.action_manual_check_mailgun()

View File

@ -13,14 +13,14 @@
<field name="email_bounced" position="after"> <field name="email_bounced" position="after">
<label for="check_email_bounced" string="Mailgun" <label for="check_email_bounced" string="Mailgun"
attrs="{'invisible': [('email', '=', False)]}"/> attrs="{'invisible': [('email', '=', False)]}"/>
<group col="3" name="mailgun_buttons" attrs="{'invisible': [('email', '=', False)]}"> <div name="mailgun_buttons" attrs="{'invisible': [('email', '=', False)]}">
<button name="check_email_bounced" type="object" string="Check Mailgun" class="oe_link"/> <button name="check_email_bounced" type="object" string="Check Mailgun" class="oe_link"/>
<button name="check_email_validity" type="object" string="Check email validity" class="oe_link"/> <button name="check_email_validity" type="object" string="Check email validity" class="oe_link"/>
<button name="force_set_bounced" type="object" string="Set Bounced" class="oe_link" <button name="force_set_bounced" type="object" string="Set Bounced" class="oe_link"
attrs="{'invisible': [('email_bounced', '=', True)]}"/> attrs="{'invisible': [('email_bounced', '=', True)]}"/>
<button name="force_unset_bounced" type="object" string="Unset Bounced" class="oe_link" <button name="force_unset_bounced" type="object" string="Unset Bounced" class="oe_link"
attrs="{'invisible': [('email_bounced', '=', False)]}"/> attrs="{'invisible': [('email_bounced', '=', False)]}"/>
</group> </div>
</field> </field>
</field> </field>
</record> </record>