Browse Source

[8.0][ADD] mail_tracking addon (#67)

* [ADD] mail_tracking addon

* Add description icon

* Fixes remarked

* Fix Travis error

* Remarks fixed
pull/269/head
Antonio Espinosa 9 years ago
committed by Jairo Llopis
parent
commit
9443c971af
  1. 112
      mail_tracking/README.rst
  2. 8
      mail_tracking/__init__.py
  3. 32
      mail_tracking/__openerp__.py
  4. 6
      mail_tracking/controllers/__init__.py
  5. 86
      mail_tracking/controllers/main.py
  6. 13
      mail_tracking/data/tracking_data.xml
  7. 24
      mail_tracking/hooks.py
  8. 430
      mail_tracking/i18n/es.po
  9. 11
      mail_tracking/models/__init__.py
  10. 94
      mail_tracking/models/ir_mail_server.py
  11. 43
      mail_tracking/models/mail_mail.py
  12. 54
      mail_tracking/models/mail_message.py
  13. 274
      mail_tracking/models/mail_tracking_email.py
  14. 136
      mail_tracking/models/mail_tracking_event.py
  15. 40
      mail_tracking/models/res_partner.py
  16. 5
      mail_tracking/security/ir.model.access.csv
  17. BIN
      mail_tracking/static/description/icon.png
  18. 13
      mail_tracking/static/src/css/mail_tracking.css
  19. BIN
      mail_tracking/static/src/img/delivered.png
  20. BIN
      mail_tracking/static/src/img/error.png
  21. BIN
      mail_tracking/static/src/img/opened.png
  22. BIN
      mail_tracking/static/src/img/sent.png
  23. BIN
      mail_tracking/static/src/img/unknown.png
  24. BIN
      mail_tracking/static/src/img/waiting.png
  25. 63
      mail_tracking/static/src/js/mail_tracking.js
  26. 61
      mail_tracking/static/src/xml/mail_tracking.xml
  27. 6
      mail_tracking/tests/__init__.py
  28. 135
      mail_tracking/tests/test_mail_tracking.py
  29. 19
      mail_tracking/views/assets.xml
  30. 122
      mail_tracking/views/mail_tracking_email_view.xml
  31. 125
      mail_tracking/views/mail_tracking_event_view.xml
  32. 33
      mail_tracking/views/res_partner_view.xml

112
mail_tracking/README.rst

@ -0,0 +1,112 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
=============
Mail tracking
=============
This module shows email notification tracking status for any messages in
mail thread (chatter). Each notified partner will have an intuitive icon just
right to his name.
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`` addon to wide load addons list
(by default, only ``web`` addon), setting ``--load`` option.
For example, ``--load=web,mail_tracking``
Usage
=====
When user sends a message in mail_thread (chatter), for instance in partner
form, then an email tracking is created for each email notification. Then a
status icon will appear just right to name of notified partner.
These are all available status icons:
.. |sent| image:: mail_tracking/static/src/img/sent.png
:width: 10px
.. |delivered| image:: mail_tracking/static/src/img/delivered.png
:width: 15px
.. |opened| image:: mail_tracking/static/src/img/opened.png
:width: 15px
.. |error| image:: mail_tracking/static/src/img/error.png
:width: 10px
.. |waiting| image:: mail_tracking/static/src/img/waiting.png
:width: 10px
.. |unknown| image:: mail_tracking/static/src/img/unknown.png
:width: 10px
|unknown| **Unknown**: No email tracking info available. Maybe this notified partner has 'Receive Inbox Notifications by Email' == 'Never'
|waiting| **Waiting**: Waiting to be sent
|error| **Error**: Error while sending
|sent| **Sent**: Sent to SMTP server configured
|delivered| **Delivered**: Delivered to final MX server
|opened| **Opened**: Opened by partner
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/205/8.0
If you want to see all tracking emails and events you can go to
* Settings > Technical > Email > Tracking emails
* Settings > Technical > Email > Tracking events
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/social/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smashing it by providing a detailed and welcomed feedback.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
* Thanks to `LlubNek <https://openclipart.org/user-detail/LlubNek>`_ and `Openclipart
<https://openclipart.org>`_ for `the icon
<https://openclipart.org/detail/19342/open-envelope>`_.
Contributors
------------
* Pedro M. Baeza <pedro.baeza@tecnativa.com>
* Antonio Espinosa <antonio.espinosa@tecnativa.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.

8
mail_tracking/__init__.py

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# flake8: noqa
from . import models
from . import controllers
from .hooks import post_init_hook

32
mail_tracking/__openerp__.py

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Email tracking",
"summary": "Email tracking system for all mails sent",
"version": "8.0.2.0.0",
"category": "Social Network",
"website": "http://www.tecnativa.com",
"author": "Tecnativa, "
"Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": [
"decimal_precision",
"mail",
],
"data": [
"data/tracking_data.xml",
"security/ir.model.access.csv",
"views/assets.xml",
"views/mail_tracking_email_view.xml",
"views/mail_tracking_event_view.xml",
"views/res_partner_view.xml",
],
"qweb": [
"static/src/xml/mail_tracking.xml",
],
"post_init_hook": "post_init_hook",
}

6
mail_tracking/controllers/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# flake8: noqa
from . import main

86
mail_tracking/controllers/main.py

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import werkzeug
from psycopg2 import OperationalError
from openerp import api, http, registry, SUPERUSER_ID
import logging
_logger = logging.getLogger(__name__)
BLANK = 'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
def _env_get(db):
reg = False
try:
reg = registry(db)
except OperationalError:
_logger.warning("Selected BD '%s' not found", db)
except: # pragma: no cover
_logger.warning("Selected BD '%s' connection error", db)
if reg:
return api.Environment(reg.cursor(), SUPERUSER_ID, {})
return False
class MailTrackingController(http.Controller):
def _request_metadata(self):
request = http.request.httprequest
return {
'ip': request.remote_addr or False,
'user_agent': request.user_agent or False,
'os_family': request.user_agent.platform or False,
'ua_family': request.user_agent.browser or False,
}
@http.route('/mail/tracking/all/<string:db>',
type='http', auth='none')
def mail_tracking_all(self, db, **kw):
env = _env_get(db)
if not env:
return 'NOT FOUND'
metadata = self._request_metadata()
response = env['mail.tracking.email'].event_process(
http.request, kw, metadata)
env.cr.commit()
env.cr.close()
return response
@http.route('/mail/tracking/event/<string:db>/<string:event_type>',
type='http', auth='none')
def mail_tracking_event(self, db, event_type, **kw):
env = _env_get(db)
if not env:
return 'NOT FOUND'
metadata = self._request_metadata()
response = env['mail.tracking.email'].event_process(
http.request, kw, metadata, event_type=event_type)
env.cr.commit()
env.cr.close()
return response
@http.route('/mail/tracking/open/<string:db>'
'/<int:tracking_email_id>/blank.gif',
type='http', auth='none')
def mail_tracking_open(self, db, tracking_email_id, **kw):
env = _env_get(db)
if env:
tracking_email = env['mail.tracking.email'].search([
('id', '=', tracking_email_id),
])
if tracking_email:
metadata = self._request_metadata()
tracking_email.event_create('open', metadata)
else:
_logger.warning(
"MailTracking email '%s' not found", tracking_email_id)
env.cr.commit()
env.cr.close()
# Always return GIF blank image
response = werkzeug.wrappers.Response()
response.mimetype = 'image/gif'
response.data = BLANK.decode('base64')
return response

13
mail_tracking/data/tracking_data.xml

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<openerp>
<data>
<record forcecreate="True" id="decimal_tracking_timestamp" model="decimal.precision">
<field name="name">MailTracking Timestamp</field>
<field name="digits">6</field>
</record>
</data>
</openerp>

24
mail_tracking/hooks.py

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
from openerp import api, SUPERUSER_ID
_logger = logging.getLogger(__name__)
def post_init_hook(cr, registry):
with api.Environment.manage():
env = api.Environment(cr, SUPERUSER_ID, {})
# Recalculate all partner tracking_email_ids
partners = env['res.partner'].search([
('email', '!=', False),
])
emails = partners.mapped('email')
_logger.info(
"Recalculating 'tracking_email_ids' in 'res.partner' "
"model for %d email addresses", len(emails))
for email in emails:
env['mail.tracking.email'].tracking_ids_recalculate(
'res.partner', 'email', 'tracking_email_ids', email)

430
mail_tracking/i18n/es.po

@ -0,0 +1,430 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mail_tracking
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-07 19:45+0000\n"
"PO-Revision-Date: 2016-06-07 19:45+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: mail_tracking
#: help:mail.tracking.email,state:0
msgid " * The 'Error' status indicates that there was an error when trying to sent the email, for example, 'No valid recipient'\n"
" * The 'Sent' status indicates that message was succesfully sent via outgoing email server (SMTP).\n"
" * The 'Delivered' status indicates that message was succesfully delivered to recipient Mail Exchange (MX) server.\n"
" * The 'Open' status indicates that message was opened or clicked by recipient.\n"
" * The 'Rejected' status indicates that recipient email address is blacklisted by outgoing email server (SMTP). It is recomended to delete this email address.\n"
" * The 'Spam' status indicates that outgoing email server (SMTP) consider this message as spam.\n"
" * The 'Unsubscribed' status indicates that recipient has requested to be unsubscribed from this message.\n"
" * The 'Bounced' status indicates that message was bounced by recipient Mail Exchange (MX) server.\n"
" * The 'Soft bounced' status indicates that message was soft bounced by recipient Mail Exchange (MX) server.\n"
""
msgstr " * 'Error' indica que ha habido un error al intentar el envío del email, por ejemplo, 'Email de destino no válido'\n"
" * 'Enviado' indica que el email se ha envíado correctamente al servidor de correo saliente (SMTP)\n"
" * 'Entregado' indica que el email se ha entregado al servidor de correo del destinatario (MX)\n"
" * 'Abierto' indica que el destinatario ha abierto o clicado en el email\n"
" * 'Rechazado' indica que la dirección del destinatario esta en una lista negra en el servidor de correo saliente (SMTP)\n"
" * 'Spam' indica que el servidor de correo saliente (SMTP) considera el email como spam\n"
" * 'Desuscrito' indica que el destinatarios ha solicitado desuscribirse desde este email\n"
" * 'Rebotado' indica que el email no ha sido aceptado por el servidor de correo del destinatario (MX)\n"
" * 'Rebotado leve' indica que el email no ha sido aceptado por motivos temporales por el servidor de correo del destinatario (MX)\n"
#. module: mail_tracking
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
msgid "Bounce"
msgstr "Rebote"
#. module: mail_tracking
#: field:mail.tracking.email,bounce_description:0
msgid "Bounce description"
msgstr "Descripción del rebote"
#. module: mail_tracking
#: field:mail.tracking.email,bounce_type:0
msgid "Bounce type"
msgstr "Tipo de rebote"
#. module: mail_tracking
#: selection:mail.tracking.email,state:0
msgid "Bounced"
msgstr "Rebotado"
#. module: mail_tracking
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
msgid "Click"
msgstr "Clic"
#. module: mail_tracking
#: selection:mail.tracking.event,event_type:0
msgid "Clicked"
msgstr "Clicado"
#. module: mail_tracking
#: field:mail.tracking.event,url:0
msgid "Clicked URL"
msgstr "URL clicada"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_form
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_tree
msgid "Country"
msgstr "País"
#. module: mail_tracking
#: field:mail.tracking.email,create_uid:0
#: field:mail.tracking.event,create_uid:0
msgid "Created by"
msgstr "Creado por"
#. module: mail_tracking
#: field:mail.tracking.email,create_date:0
#: field:mail.tracking.event,create_date:0
msgid "Created on"
msgstr "Creado en"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
#: field:mail.tracking.email,date:0
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
#: field:mail.tracking.event,date:0
msgid "Date"
msgstr "Fecha"
#. module: mail_tracking
#: selection:mail.tracking.event,event_type:0
msgid "Deferral"
msgstr "Retraso"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
#: selection:mail.tracking.email,state:0
msgid "Deferred"
msgstr "Retrasado"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
#: selection:mail.tracking.email,state:0
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
#: selection:mail.tracking.event,event_type:0
msgid "Delivered"
msgstr "Entregado"
#. module: mail_tracking
#: field:mail.tracking.email,display_name:0
#: field:mail.tracking.event,display_name:0
msgid "Display Name"
msgstr "Nombre a mostrar"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
#: field:mail.tracking.email,mail_id:0
msgid "Email"
msgstr "Correo electrónico"
#. module: mail_tracking
#: selection:mail.tracking.email,state:0
msgid "Error"
msgstr "Error"
#. module: mail_tracking
#: field:mail.tracking.email,error_smtp_server:0
msgid "Error SMTP server"
msgstr "Error del servidor SMTP"
#. module: mail_tracking
#: field:mail.tracking.email,error_description:0
msgid "Error description"
msgstr "Descripción del error"
#. module: mail_tracking
#: field:mail.tracking.email,error_type:0
msgid "Error type"
msgstr "Tipo de error"
#. module: mail_tracking
#: field:mail.tracking.event,event_type:0
msgid "Event type"
msgstr "Tipo de evento"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
msgid "Failed"
msgstr "Ha fallado"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
msgid "Group By"
msgstr "Agrupar por"
#. module: mail_tracking
#: selection:mail.tracking.event,event_type:0
msgid "Hard bounce"
msgstr "Rebote duro"
#. module: mail_tracking
#: field:mail.tracking.email,id:0
#: field:mail.tracking.event,id:0
msgid "ID"
msgstr "ID"
#. module: mail_tracking
#: field:mail.tracking.event,mobile:0
msgid "Is mobile?"
msgstr "¿Es desde móvil?"
#. module: mail_tracking
#: field:mail.tracking.email,__last_update:0
#: field:mail.tracking.event,__last_update:0
msgid "Last Modified on"
msgstr "Última modificación en"
#. module: mail_tracking
#: field:mail.tracking.email,write_uid:0
#: field:mail.tracking.event,write_uid:0
msgid "Last Updated by"
msgstr "Última modificación por"
#. module: mail_tracking
#: field:mail.tracking.email,write_date:0
#: field:mail.tracking.event,write_date:0
msgid "Last Updated on"
msgstr "Última actualización en"
#. module: mail_tracking
#: model:ir.model,name:mail_tracking.model_mail_tracking_email
msgid "MailTracking email"
msgstr "MailTracking email"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
msgid "MailTracking email search"
msgstr "MailTracking email search"
#. module: mail_tracking
#: model:ir.actions.act_window,name:mail_tracking.action_view_mail_tracking_email
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_tree
msgid "MailTracking emails"
msgstr "MailTracking emails"
#. module: mail_tracking
#: model:ir.model,name:mail_tracking.model_mail_tracking_event
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_form
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_form
msgid "MailTracking event"
msgstr "MailTracking event"
#. module: mail_tracking
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
msgid "MailTracking event search"
msgstr "MailTracking event search"
#. module: mail_tracking
#: model:ir.actions.act_window,name:mail_tracking.action_view_mail_tracking_event
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_tree
msgid "MailTracking events"
msgstr "MailTracking events"
#. module: mail_tracking
#: model:ir.model,name:mail_tracking.model_mail_message
#: field:mail.tracking.email,mail_message_id:0
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
#: field:mail.tracking.event,tracking_email_id:0
msgid "Message"
msgstr "Mensaje"
#. module: mail_tracking
#. openerp-web
#: code:addons/mail_tracking/static/src/js/mail_tracking.js:31
#, python-format
msgid "Message tracking"
msgstr "Seguimiento de mensaje"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
msgid "Month"
msgstr "Mes"
#. module: mail_tracking
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
msgid "Open"
msgstr "Abrir"
#. module: mail_tracking
#: selection:mail.tracking.email,state:0
#: selection:mail.tracking.event,event_type:0
msgid "Open"
msgstr "Abierto"
#. module: mail_tracking
#: field:mail.tracking.event,os_family:0
msgid "Operating system family"
msgstr "Familia de sistema operativo"
#. module: mail_tracking
#: model:ir.model,name:mail_tracking.model_mail_mail
msgid "Outgoing Mails"
msgstr "Emails de salida"
#. module: mail_tracking
#: field:mail.tracking.email,partner_id:0
msgid "Partner"
msgstr "Empresa"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_tree
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
#: field:mail.tracking.event,recipient:0
msgid "Recipient"
msgstr "Destinatario"
#. module: mail_tracking
#: field:mail.tracking.email,recipient:0
msgid "Recipient email"
msgstr "Email del destinatario"
#. module: mail_tracking
#: selection:mail.tracking.email,state:0
#: selection:mail.tracking.event,event_type:0
msgid "Rejected"
msgstr "Rechazado"
#. module: mail_tracking
#: field:mail.tracking.event,smtp_server:0
msgid "SMTP server"
msgstr "Servidor SMTP"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_tree
msgid "Sender"
msgstr "Remitente"
#. module: mail_tracking
#: field:mail.tracking.email,sender:0
msgid "Sender email"
msgstr "Email del remitente"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
#: selection:mail.tracking.email,state:0
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
#: selection:mail.tracking.event,event_type:0
msgid "Sent"
msgstr "Enviar"
#. module: mail_tracking
#: selection:mail.tracking.event,event_type:0
msgid "Soft bounce"
msgstr "Rebote suave"
#. module: mail_tracking
#: selection:mail.tracking.email,state:0
msgid "Soft bounced"
msgstr "Rebotado suave"
#. module: mail_tracking
#: selection:mail.tracking.email,state:0
#: selection:mail.tracking.event,event_type:0
msgid "Spam"
msgstr "Spam"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
#: field:mail.tracking.email,state:0
msgid "State"
msgstr "Estado"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
#: field:mail.tracking.email,name:0
msgid "Subject"
msgstr "Asunto"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
#: field:mail.tracking.email,time:0
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
#: field:mail.tracking.event,time:0
msgid "Time"
msgstr "Hora"
#. module: mail_tracking
#: model:ir.ui.menu,name:mail_tracking.menu_mail_tracking_email
msgid "Tracking emails"
msgstr "Emails de seguimiento"
#. module: mail_tracking
#: model:ir.ui.menu,name:mail_tracking.menu_mail_tracking_event
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_form
#: field:mail.tracking.email,tracking_event_ids:0
msgid "Tracking events"
msgstr "Eventos de seguimiento"
#. module: mail_tracking
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
msgid "Type"
msgstr "Tipo"
#. module: mail_tracking
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
msgid "URL"
msgstr "URL"
#. module: mail_tracking
#: field:mail.tracking.email,timestamp:0
#: field:mail.tracking.event,timestamp:0
msgid "UTC timestamp"
msgstr "Tiempo UTC"
#. module: mail_tracking
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
msgid "Unsubscribe"
msgstr "Desuscribirse"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_search
#: selection:mail.tracking.email,state:0
#: selection:mail.tracking.event,event_type:0
msgid "Unsubscribed"
msgstr "Desuscrito"
#. module: mail_tracking
#: field:mail.tracking.event,ip:0
msgid "User IP"
msgstr "IP usuario"
#. module: mail_tracking
#: view:mail.tracking.email:mail_tracking.view_mail_tracking_email_form
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_tree
#: field:mail.tracking.event,user_agent:0
msgid "User agent"
msgstr "Aplicación del usuario"
#. module: mail_tracking
#: field:mail.tracking.event,ua_family:0
msgid "User agent family"
msgstr "Familia de la aplicación del usuario"
#. module: mail_tracking
#: view:mail.tracking.event:mail_tracking.view_mail_tracking_event_search
#: field:mail.tracking.event,ua_type:0
msgid "User agent type"
msgstr "Tipo de aplicación del usuario"
#. module: mail_tracking
#: field:mail.tracking.event,user_country_id:0
msgid "User country"
msgstr "País del usuario"

11
mail_tracking/models/__init__.py

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# flake8: noqa
from . import ir_mail_server
from . import mail_mail
from . import mail_message
from . import mail_tracking_email
from . import mail_tracking_event
from . import res_partner

94
mail_tracking/models/ir_mail_server.py

@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import re
import threading
from openerp import models, api, tools
class IrMailServer(models.Model):
_inherit = "ir.mail_server"
def _tracking_headers_add(self, tracking_email_id, headers):
"""Allow other addons to add its own tracking SMTP headers"""
headers = headers or {}
headers['X-Odoo-Database'] = getattr(
threading.currentThread(), 'dbname', None),
headers['X-Odoo-Tracking-ID'] = tracking_email_id
return headers
def _tracking_email_id_body_get(self, body):
body = body or ''
tracking_email_id = False
# https://regex101.com/r/lW4cB1/2
match = re.search(
r'<img [^>]* data-odoo-tracking-email=["\']([0-9]*)["\']', body)
if match:
try:
tracking_email_id = int(match.group(1))
except: # pragma: no cover
pass
return tracking_email_id
def build_email(self, email_from, email_to, subject, body, email_cc=None,
email_bcc=None, reply_to=False, attachments=None,
message_id=None, references=None, object_id=False,
subtype='plain', headers=None, body_alternative=None,
subtype_alternative='plain'):
tracking_email_id = self._tracking_email_id_body_get(body)
if tracking_email_id:
headers = self._tracking_headers_add(tracking_email_id, headers)
msg = super(IrMailServer, self).build_email(
email_from, email_to, subject, body, email_cc=email_cc,
email_bcc=email_bcc, reply_to=reply_to, attachments=attachments,
message_id=message_id, references=references, object_id=object_id,
subtype=subtype, headers=headers,
body_alternative=body_alternative,
subtype_alternative=subtype_alternative)
return msg
def _tracking_email_get(self, message):
tracking_email_id = False
if message.get('X-Odoo-Tracking-ID', '').isdigit():
tracking_email_id = int(message['X-Odoo-Tracking-ID'])
return self.env['mail.tracking.email'].browse(tracking_email_id)
def _smtp_server_get(self, mail_server_id, smtp_server):
smtp_server_used = False
mail_server = None
if mail_server_id:
mail_server = self.browse(mail_server_id)
elif not smtp_server:
mail_server_ids = self.search([], order='sequence', limit=1)
mail_server = mail_server_ids[0] if mail_server_ids else None
if mail_server:
smtp_server_used = mail_server.smtp_host
else: # pragma: no cover
smtp_server_used = smtp_server or tools.config.get('smtp_server')
return smtp_server_used
@api.model
def send_email(self, message, mail_server_id=None, smtp_server=None,
smtp_port=None, smtp_user=None, smtp_password=None,
smtp_encryption=None, smtp_debug=False):
message_id = False
tracking_email = self._tracking_email_get(message)
smtp_server_used = self._smtp_server_get(
mail_server_id, smtp_server)
try:
message_id = super(IrMailServer, self).send_email(
message, mail_server_id=mail_server_id,
smtp_server=smtp_server, smtp_port=smtp_port,
smtp_user=smtp_user, smtp_password=smtp_password,
smtp_encryption=smtp_encryption, smtp_debug=smtp_debug)
except Exception as e:
if tracking_email:
tracking_email.smtp_error(self, smtp_server_used, e)
raise
if message_id and tracking_email:
vals = tracking_email._tracking_sent_prepare(
self, smtp_server_used, message, message_id)
if vals:
self.env['mail.tracking.event'].sudo().create(vals)
return message_id

43
mail_tracking/models/mail_mail.py

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import time
from datetime import datetime
from email.utils import COMMASPACE
from openerp import models, api, fields
class MailMail(models.Model):
_inherit = 'mail.mail'
@api.model
def _tracking_email_prepare(self, mail, partner, email):
ts = time.time()
dt = datetime.utcfromtimestamp(ts)
email_to_list = email.get('email_to', [])
email_to = COMMASPACE.join(email_to_list)
return {
'name': email.get('subject', False),
'timestamp': '%.6f' % ts,
'time': fields.Datetime.to_string(dt),
'mail_id': mail.id if mail else False,
'mail_message_id': mail.mail_message_id.id if mail else False,
'partner_id': partner.id if partner else False,
'recipient': email_to,
'sender': mail.email_from,
}
@api.model
def send_get_email_dict(self, mail, partner=None):
email = super(MailMail, self).send_get_email_dict(
mail, partner=partner)
m_tracking = self.env['mail.tracking.email']
tracking_email = False
if mail:
vals = self._tracking_email_prepare(mail, partner, email)
tracking_email = m_tracking.sudo().create(vals)
if tracking_email:
email = tracking_email.tracking_img_add(email)
return email

54
mail_tracking/models/mail_message.py

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import models, api
import logging
_logger = logging.getLogger(__name__)
class MailMessage(models.Model):
_inherit = "mail.message"
def _tracking_status_map_get(self):
return {
'False': 'waiting',
'error': 'error',
'deferred': 'sent',
'sent': 'sent',
'delivered': 'delivered',
'opened': 'opened',
'rejected': 'error',
'spam': 'error',
'unsub': 'opened',
'bounced': 'error',
'soft-bounced': 'error',
}
def _partner_tracking_status_get(self, tracking_email):
tracking_status_map = self._tracking_status_map_get()
status = 'unknown'
if tracking_email:
tracking_email_status = str(tracking_email.state)
status = tracking_status_map.get(tracking_email_status, 'unknown')
return status
@api.model
def _message_read_dict_postprocess(self, messages, message_tree):
res = super(MailMessage, self)._message_read_dict_postprocess(
messages, message_tree)
for message_dict in messages:
mail_message_id = message_dict.get('id', False)
if mail_message_id:
partner_trackings = {}
for partner in message_dict.get('partner_ids', []):
partner_id = partner[0]
tracking_email = self.env['mail.tracking.email'].search([
('mail_message_id', '=', mail_message_id),
('partner_id', '=', partner_id),
])
status = self._partner_tracking_status_get(tracking_email)
partner_trackings[str(partner_id)] = (
status, tracking_email.id)
message_dict['partner_trackings'] = partner_trackings
return res

274
mail_tracking/models/mail_tracking_email.py

@ -0,0 +1,274 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
import urlparse
import time
import re
from datetime import datetime
from openerp import models, api, fields, tools
import openerp.addons.decimal_precision as dp
_logger = logging.getLogger(__name__)
class MailTrackingEmail(models.Model):
_name = "mail.tracking.email"
_order = 'time desc'
_rec_name = 'display_name'
_description = 'MailTracking email'
name = fields.Char(string="Subject", readonly=True, index=True)
display_name = fields.Char(
string="Display name", readonly=True, store=True,
compute="_compute_display_name")
timestamp = fields.Float(
string='UTC timestamp', readonly=True,
digits=dp.get_precision('MailTracking Timestamp'))
time = fields.Datetime(string="Time", readonly=True)
date = fields.Date(
string="Date", readonly=True, compute="_compute_date", store=True)
mail_message_id = fields.Many2one(
string="Message", comodel_name='mail.message', readonly=True)
mail_id = fields.Many2one(
string="Email", comodel_name='mail.mail', readonly=True)
partner_id = fields.Many2one(
string="Partner", comodel_name='res.partner', readonly=True)
recipient = fields.Char(string='Recipient email', readonly=True)
recipient_address = fields.Char(
string='Recipient email address', readonly=True, store=True,
compute='_compute_recipient_address')
sender = fields.Char(string='Sender email', readonly=True)
state = fields.Selection([
('error', 'Error'),
('deferred', 'Deferred'),
('sent', 'Sent'),
('delivered', 'Delivered'),
('opened', 'Open'),
('rejected', 'Rejected'),
('spam', 'Spam'),
('unsub', 'Unsubscribed'),
('bounced', 'Bounced'),
('soft-bounced', 'Soft bounced'),
], string='State', index=True, readonly=True, default=False,
help=" * The 'Error' status indicates that there was an error "
"when trying to sent the email, for example, "
"'No valid recipient'\n"
" * The 'Sent' status indicates that message was succesfully "
"sent via outgoing email server (SMTP).\n"
" * The 'Delivered' status indicates that message was "
"succesfully delivered to recipient Mail Exchange (MX) server.\n"
" * The 'Open' status indicates that message was opened or "
"clicked by recipient.\n"
" * The 'Rejected' status indicates that recipient email "
"address is blacklisted by outgoing email server (SMTP). "
"It is recomended to delete this email address.\n"
" * The 'Spam' status indicates that outgoing email "
"server (SMTP) consider this message as spam.\n"
" * The 'Unsubscribed' status indicates that recipient has "
"requested to be unsubscribed from this message.\n"
" * The 'Bounced' status indicates that message was bounced "
"by recipient Mail Exchange (MX) server.\n"
" * The 'Soft bounced' status indicates that message was soft "
"bounced by recipient Mail Exchange (MX) server.\n")
error_smtp_server = fields.Char(string='Error SMTP server', readonly=True)
error_type = fields.Char(string='Error type', readonly=True)
error_description = fields.Char(
string='Error description', readonly=True)
bounce_type = fields.Char(string='Bounce type', readonly=True)
bounce_description = fields.Char(
string='Bounce description', readonly=True)
tracking_event_ids = fields.One2many(
string="Tracking events", comodel_name='mail.tracking.event',
inverse_name='tracking_email_id', readonly=True)
@api.model
def tracking_ids_recalculate(self, model, email_field, tracking_field,
email, new_tracking=None):
objects = self.env[model].search([
(email_field, '=ilike', email),
])
for obj in objects:
trackings = obj[tracking_field]
if new_tracking:
trackings |= new_tracking
trackings = trackings._email_score_tracking_filter()
if set(obj[tracking_field].ids) != set(trackings.ids):
if trackings:
obj.write({
tracking_field: [(6, False, trackings.ids)]
})
else:
obj.write({
tracking_field: [(5, False, False)]
})
return True
@api.model
def _tracking_ids_to_write(self, email):
trackings = self.env['mail.tracking.email'].search([
('recipient_address', '=ilike', email)
])
trackings = trackings._email_score_tracking_filter()
if trackings:
return [(6, False, trackings.ids)]
else:
return [(5, False, False)]
@api.multi
def _email_score_tracking_filter(self):
"""Default email score filter for tracking emails"""
# Consider only last 10 tracking emails
return self.sorted(key=lambda r: r.time, reverse=True)[:10]
@api.multi
def email_score(self):
"""Default email score algorimth"""
score = 50.0
trackings = self._email_score_tracking_filter()
for tracking in trackings:
if tracking.state in ('error',):
score -= 50.0
elif tracking.state in ('rejected', 'spam', 'bounced'):
score -= 25.0
elif tracking.state in ('soft-bounced', 'unsub'):
score -= 10.0
elif tracking.state in ('delivered',):
score += 5.0
elif tracking.state in ('opened',):
score += 10.0
if score > 100.0:
score = 100.0
return score
@api.multi
@api.depends('recipient')
def _compute_recipient_address(self):
for email in self:
matches = re.search(r'<(.*@.*)>', email.recipient)
if matches:
email.recipient_address = matches.group(1)
else:
email.recipient_address = email.recipient
@api.multi
@api.depends('name', 'recipient')
def _compute_display_name(self):
for email in self:
parts = [email.name]
if email.recipient:
parts.append(email.recipient)
email.display_name = ' - '.join(parts)
@api.multi
@api.depends('time')
def _compute_date(self):
for email in self:
email.date = fields.Date.to_string(
fields.Date.from_string(email.time))
@api.model
def create(self, vals):
tracking = super(MailTrackingEmail, self).create(vals)
self.tracking_ids_recalculate(
'res.partner', 'email', 'tracking_email_ids',
tracking.recipient_address, new_tracking=tracking)
return tracking
def _get_mail_tracking_img(self):
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
path_url = (
'mail/tracking/open/%(db)s/%(tracking_email_id)s/blank.gif' % {
'db': self.env.cr.dbname,
'tracking_email_id': self.id,
})
track_url = urlparse.urljoin(base_url, path_url)
return (
'<img src="%(url)s" alt="" '
'data-odoo-tracking-email="%(tracking_email_id)s"/>' % {
'url': track_url,
'tracking_email_id': self.id,
})
@api.multi
def smtp_error(self, mail_server, smtp_server, exception):
self.sudo().write({
'error_smtp_server': tools.ustr(smtp_server),
'error_type': exception.__class__.__name__,
'error_description': tools.ustr(exception),
'state': 'error',
})
return True
@api.multi
def tracking_img_add(self, email):
self.ensure_one()
tracking_url = self._get_mail_tracking_img()
if tracking_url:
body = tools.append_content_to_html(
email.get('body', ''), tracking_url, plaintext=False,
container_tag='div')
email['body'] = body
return email
def _message_partners_check(self, message, message_id):
mail_message = self.mail_message_id
partners = mail_message.notified_partner_ids | mail_message.partner_ids
if (self.partner_id and self.partner_id not in partners):
# If mail_message haven't tracking partner, then
# add it in order to see his trackking status in chatter
if mail_message.subtype_id:
mail_message.sudo().write({
'notified_partner_ids': [(4, self.partner_id.id)],
})
else:
mail_message.sudo().write({
'partner_ids': [(4, self.partner_id.id)],
})
return True
@api.multi
def _tracking_sent_prepare(self, mail_server, smtp_server, message,
message_id):
self.ensure_one()
ts = time.time()
dt = datetime.utcfromtimestamp(ts)
self._message_partners_check(message, message_id)
self.sudo().write({'state': 'sent'})
return {
'recipient': message['To'],
'timestamp': '%.6f' % ts,
'time': fields.Datetime.to_string(dt),
'tracking_email_id': self.id,
'event_type': 'sent',
'smtp_server': smtp_server,
}
def _event_prepare(self, event_type, metadata):
self.ensure_one()
m_event = self.env['mail.tracking.event']
method = getattr(m_event, 'process_' + event_type, None)
if method and hasattr(method, '__call__'):
return method(self, metadata)
else: # pragma: no cover
_logger.info('Unknown event type: %s' % event_type)
return False
@api.multi
def event_create(self, event_type, metadata):
event_ids = self.env['mail.tracking.event']
for tracking_email in self:
vals = tracking_email._event_prepare(event_type, metadata)
if vals:
event_ids += event_ids.sudo().create(vals)
return event_ids
@api.model
def event_process(self, request, post, metadata, event_type=None):
# Generic event process hook, inherit it and
# - return 'OK' if processed
# - return 'NONE' if this request is not for you
# - return 'ERROR' if any error
return 'NONE' # pragma: no cover

136
mail_tracking/models/mail_tracking_event.py

@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import time
from datetime import datetime
from openerp import models, api, fields
import openerp.addons.decimal_precision as dp
class MailTrackingEvent(models.Model):
_name = "mail.tracking.event"
_order = 'timestamp desc'
_rec_name = 'event_type'
_description = 'MailTracking event'
recipient = fields.Char(string="Recipient", readonly=True)
timestamp = fields.Float(
string='UTC timestamp', readonly=True,
digits=dp.get_precision('MailTracking Timestamp'))
time = fields.Datetime(string="Time", readonly=True)
date = fields.Date(
string="Date", readonly=True, compute="_compute_date", store=True)
tracking_email_id = fields.Many2one(
string='Message', readonly=True,
comodel_name='mail.tracking.email')
event_type = fields.Selection(string='Event type', selection=[
('sent', 'Sent'),
('delivered', 'Delivered'),
('deferral', 'Deferral'),
('hard_bounce', 'Hard bounce'),
('soft_bounce', 'Soft bounce'),
('open', 'Open'),
('click', 'Clicked'),
('spam', 'Spam'),
('unsub', 'Unsubscribed'),
('reject', 'Rejected'),
], readonly=True)
smtp_server = fields.Char(string='SMTP server', readonly=True)
url = fields.Char(string='Clicked URL', readonly=True)
ip = fields.Char(string='User IP', readonly=True)
user_agent = fields.Char(string='User agent', readonly=True)
mobile = fields.Boolean(string='Is mobile?', readonly=True)
os_family = fields.Char(string='Operating system family', readonly=True)
ua_family = fields.Char(string='User agent family', readonly=True)
ua_type = fields.Char(string='User agent type', readonly=True)
user_country_id = fields.Many2one(string='User country', readonly=True,
comodel_name='res.country')
error_type = fields.Char(string='Error type', readonly=True)
error_description = fields.Char(string='Error description', readonly=True)
error_details = fields.Text(string='Error details', readonly=True)
@api.multi
@api.depends('time')
def _compute_date(self):
for email in self:
email.date = fields.Date.to_string(
fields.Date.from_string(email.time))
def _process_data(self, tracking_email, metadata, event_type, state):
ts = time.time()
dt = datetime.utcfromtimestamp(ts)
return {
'recipient': metadata.get('recipient', tracking_email.recipient),
'timestamp': metadata.get('timestamp', ts),
'time': metadata.get('time', fields.Datetime.to_string(dt)),
'date': metadata.get('date', fields.Date.to_string(dt)),
'tracking_email_id': tracking_email.id,
'event_type': event_type,
'ip': metadata.get('ip', False),
'url': metadata.get('url', False),
'user_agent': metadata.get('user_agent', False),
'mobile': metadata.get('mobile', False),
'os_family': metadata.get('os_family', False),
'ua_family': metadata.get('ua_family', False),
'ua_type': metadata.get('ua_type', False),
'user_country_id': metadata.get('user_country_id', False),
'error_type': metadata.get('error_type', False),
'error_description': metadata.get('error_description', False),
'error_details': metadata.get('error_details', False),
}
def _process_status(self, tracking_email, metadata, event_type, state):
tracking_email.sudo().write({'state': state})
return self._process_data(tracking_email, metadata, event_type, state)
def _process_bounce(self, tracking_email, metadata, event_type, state):
tracking_email.sudo().write({
'state': state,
'bounce_type': metadata.get('bounce_type', False),
'bounce_description': metadata.get('bounce_description', False),
})
return self._process_data(tracking_email, metadata, event_type, state)
@api.model
def process_delivered(self, tracking_email, metadata):
return self._process_status(
tracking_email, metadata, 'delivered', 'delivered')
@api.model
def process_deferral(self, tracking_email, metadata):
return self._process_status(
tracking_email, metadata, 'deferral', 'deferred')
@api.model
def process_hard_bounce(self, tracking_email, metadata):
return self._process_bounce(
tracking_email, metadata, 'hard_bounce', 'bounced')
@api.model
def process_soft_bounce(self, tracking_email, metadata):
return self._process_bounce(
tracking_email, metadata, 'soft_bounce', 'soft-bounced')
@api.model
def process_open(self, tracking_email, metadata):
return self._process_status(tracking_email, metadata, 'open', 'opened')
@api.model
def process_click(self, tracking_email, metadata):
return self._process_status(
tracking_email, metadata, 'click', 'opened')
@api.model
def process_spam(self, tracking_email, metadata):
return self._process_status(tracking_email, metadata, 'spam', 'spam')
@api.model
def process_unsub(self, tracking_email, metadata):
return self._process_status(tracking_email, metadata, 'unsub', 'unsub')
@api.model
def process_reject(self, tracking_email, metadata):
return self._process_status(
tracking_email, metadata, 'reject', 'rejected')

40
mail_tracking/models/res_partner.py

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import models, api, fields
class ResPartner(models.Model):
_inherit = 'res.partner'
tracking_email_ids = fields.Many2many(
string="Tracking emails", comodel_name="mail.tracking.email",
readonly=True)
tracking_emails_count = fields.Integer(
string="Tracking emails count", store=True, readonly=True,
compute="_compute_tracking_emails_count")
email_score = fields.Float(
string="Email score",
compute="_compute_email_score", store=True, readonly=True)
@api.one
@api.depends('tracking_email_ids.state')
def _compute_email_score(self):
self.email_score = self.tracking_email_ids.email_score()
@api.one
@api.depends('tracking_email_ids')
def _compute_tracking_emails_count(self):
self.tracking_emails_count = self.env['mail.tracking.email'].\
search_count([
('recipient_address', '=ilike', self.email)
])
@api.multi
def write(self, vals):
email = vals.get('email')
if email is not None:
vals['tracking_email_ids'] = \
self.env['mail.tracking.email']._tracking_ids_to_write(email)
return super(ResPartner, self).write(vals)

5
mail_tracking/security/ir.model.access.csv

@ -0,0 +1,5 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_mail_tracking_email_group_user","mail_tracking_email group_user","model_mail_tracking_email","base.group_user",1,0,0,0
"access_mail_tracking_event_group_user","mail_tracking_event group_user","model_mail_tracking_event","base.group_user",1,0,0,0
"access_mail_tracking_email_group_system","mail_tracking_email group_system","model_mail_tracking_email","base.group_system",1,1,1,1
"access_mail_tracking_event_group_system","mail_tracking_event group_system","model_mail_tracking_event","base.group_system",1,1,1,1

BIN
mail_tracking/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 5.8 KiB

13
mail_tracking/static/src/css/mail_tracking.css

@ -0,0 +1,13 @@
/* © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */
.mail_tracking span {
color: #909090;
}
.mail_tracking_pointer {
cursor: pointer;
}
.mail_tracking span.mail_tracking_opened {
color: #a34a8b;
}

BIN
mail_tracking/static/src/img/delivered.png

After

Width: 23  |  Height: 16  |  Size: 285 B

BIN
mail_tracking/static/src/img/error.png

After

Width: 16  |  Height: 16  |  Size: 257 B

BIN
mail_tracking/static/src/img/opened.png

After

Width: 23  |  Height: 16  |  Size: 368 B

BIN
mail_tracking/static/src/img/sent.png

After

Width: 16  |  Height: 16  |  Size: 294 B

BIN
mail_tracking/static/src/img/unknown.png

After

Width: 16  |  Height: 16  |  Size: 425 B

BIN
mail_tracking/static/src/img/waiting.png

After

Width: 16  |  Height: 16  |  Size: 348 B

63
mail_tracking/static/src/js/mail_tracking.js

@ -0,0 +1,63 @@
/* © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */
(function ($, window, document) {
'use strict';
openerp.mail_tracking = function (instance) {
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
var mail_orig = instance.mail;
var mail_inherit = function() {
instance.mail.MessageCommon.include({
init: function (parent, datasets, options) {
this._super(parent, datasets, options);
this.partner_trackings = datasets.partner_trackings || [];
}
});
instance.mail.ThreadMessage.include({
bind_events: function () {
this._super();
this.$('.oe_mail_action_tracking').on('click', this.on_tracking_status_clicked);
},
on_tracking_status_clicked: function (event) {
event.preventDefault();
var tracking_email_id = $(event.delegateTarget).data('tracking');
var state = {
'model': 'mail.tracking.email',
'id': tracking_email_id,
'title': _t("Message tracking"),
};
instance.webclient.action_manager.do_push_state(state);
console.log('tracking_email_id = ' + tracking_email_id);
var action = {
type:'ir.actions.act_window',
view_type: 'form',
view_mode: 'form',
res_model: 'mail.tracking.email',
views: [[false, 'form']],
target: 'new',
res_id: tracking_email_id,
};
this.do_action(action);
}
});
};
// Tricky way to guarantee that this module is loaded always
// after mail module.
// When --load=web,mail_tracking is specified in init script, then
// web and mail_tracking are the first modules to load in JS
if (instance.mail.MessageCommon === undefined) {
instance.mail = function(instance) {
instance.mail = mail_orig;
instance.mail(instance, instance.mail);
mail_inherit();
};
} else {
mail_inherit();
}
};
}(window.jQuery, window, document));

61
mail_tracking/static/src/xml/mail_tracking.xml

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<template>
<t t-name="mail.tracking.status">
<t t-if="tracking[0] == 'unknown'">
<span class="mail_tracking_unknown">
<i class="fa fa-ban"></i>
</span>
</t>
<t t-if="tracking[0] == 'waiting'">
<span class="mail_tracking_waiting mail_tracking_pointer">
<i class="fa fa-clock-o"></i>
</span>
</t>
<t t-if="tracking[0] == 'error'">
<span class="mail_tracking_error mail_tracking_pointer">
<i class="fa fa-remove"></i>
</span>
</t>
<t t-if="tracking[0] == 'sent'">
<span class="mail_tracking_sent mail_tracking_pointer">
<i class="fa fa-check"></i>
</span>
</t>
<t t-if="tracking[0] == 'delivered'">
<span class="fa-stack mail_tracking_delivered mail_tracking_pointer">
<i class="fa fa-check fa-stack-1x" style="margin-left:1px"></i>
<i class="fa fa-check fa-inverse fa-stack-1x" style="margin-left:-2px;"></i>
<i class="fa fa-check fa-stack-1x" style="margin-left:-3px"></i>
</span>
</t>
<t t-if="tracking[0] == 'opened'">
<span class="fa-stack mail_tracking_opened mail_tracking_pointer">
<i class="fa fa-check fa-stack-1x" style="margin-left:1px"></i>
<i class="fa fa-check fa-inverse fa-stack-1x" style="margin-left:-2px;"></i>
<i class="fa fa-check fa-stack-1x" style="margin-left:-3px"></i>
</span>
</t>
</t>
<t t-extend="mail.thread.message">
<t t-jquery="span[t-attf-class='oe_partner_follower']" t-operation="append">
<t t-set='tracking' t-value='widget.partner_trackings[partner[0]]'/>
<t t-if="tracking[1]">
<span class="mail_tracking oe_mail_action_tracking"
t-att-data-tracking="tracking[1]"
t-attf-title="Status: #{tracking[0]}">
<t t-call="mail.tracking.status"/>
</span>
</t>
<t t-if="!tracking[1]">
<span class="mail_tracking" t-attf-title="Status: #{tracking[0]}">
<t t-call="mail.tracking.status"/>
</span>
</t>
</t>
</t>
</template>

6
mail_tracking/tests/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# flake8: noqa
from . import test_mail_tracking

135
mail_tracking/tests/test_mail_tracking.py

@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import mock
import base64
from openerp.tests.common import TransactionCase
from openerp.addons.mail_tracking.controllers.main import \
MailTrackingController, BLANK
mock_request = 'openerp.http.request'
mock_send_email = ('openerp.addons.base.ir.ir_mail_server.'
'ir_mail_server.send_email')
class FakeUserAgent(object):
browser = 'Test browser'
platform = 'Test platform'
def __str__(self):
"""Return name"""
return 'Test suite'
# One test case per method
class TestMailTracking(TransactionCase):
# Use case : Prepare some data for current test case
def setUp(self):
super(TestMailTracking, self).setUp()
self.sender = self.env['res.partner'].create({
'name': 'Test sender',
'email': 'sender@example.com',
'notify_email': 'always',
})
self.recipient = self.env['res.partner'].create({
'name': 'Test recipient',
'email': 'recipient@example.com',
'notify_email': 'always',
})
self.request = {
'httprequest': type('obj', (object,), {
'remote_addr': '123.123.123.123',
'user_agent': FakeUserAgent(),
}),
}
def test_message_post(self):
# This message will generate a notification for recipient
message = self.env['mail.message'].create({
'subject': 'Message test',
'author_id': self.sender.id,
'email_from': self.sender.email,
'type': 'comment',
'model': 'res.partner',
'res_id': self.recipient.id,
'partner_ids': [(4, self.recipient.id)],
'body': '<p>This is a test message</p>',
})
# Search tracking created
tracking_email = self.env['mail.tracking.email'].search([
('mail_message_id', '=', message.id),
('partner_id', '=', self.recipient.id),
])
# The tracking email must be sent
self.assertTrue(tracking_email)
self.assertEqual(tracking_email.state, 'sent')
# message_dict read by web interface
message_dict = self.env['mail.message'].message_read(message.id)
# First item is message content
self.assertTrue(len(message_dict) > 0)
message_dict = message_dict[0]
self.assertTrue(len(message_dict['partner_ids']) > 0)
# First partner is recipient
partner_id = message_dict['partner_ids'][0][0]
self.assertEqual(partner_id, self.recipient.id)
status = message_dict['partner_trackings'][str(partner_id)]
# Tracking status must be sent and
# mail tracking must be the one search before
self.assertEqual(status[0], 'sent')
self.assertEqual(status[1], tracking_email.id)
# And now open the email
metadata = {
'ip': '127.0.0.1',
'user_agent': 'Odoo Test/1.0',
'os_family': 'linux',
'ua_family': 'odoo',
}
tracking_email.event_create('open', metadata)
self.assertEqual(tracking_email.state, 'opened')
def mail_send(self):
mail = self.env['mail.mail'].create({
'subject': 'Test subject',
'email_from': 'from@domain.com',
'email_to': 'to@domain.com',
'body_html': '<p>This is a test message</p>',
})
mail.send()
# Search tracking created
tracking_email = self.env['mail.tracking.email'].search([
('mail_id', '=', mail.id),
])
return mail, tracking_email
def test_mail_send(self):
controller = MailTrackingController()
db = self.env.cr.dbname
image = base64.decodestring(BLANK)
mail, tracking = self.mail_send()
self.assertEqual(mail.email_to, tracking.recipient)
self.assertEqual(mail.email_from, tracking.sender)
with mock.patch(mock_request) as mock_func:
mock_func.return_value = type('obj', (object,), self.request)
res = controller.mail_tracking_open(db, tracking.id)
self.assertEqual(image, res.response[0])
def test_smtp_error(self):
with mock.patch(mock_send_email) as mock_func:
mock_func.side_effect = Warning('Test error')
mail, tracking = self.mail_send()
self.assertEqual('error', tracking.state)
self.assertEqual('Warning', tracking.error_type)
self.assertEqual('Test error', tracking.error_description)
def test_db(self):
db = self.env.cr.dbname
controller = MailTrackingController()
with mock.patch(mock_request) as mock_func:
mock_func.return_value = type('obj', (object,), self.request)
not_found = controller.mail_tracking_all('not_found_db')
self.assertEqual('NOT FOUND', not_found.response[0])
none = controller.mail_tracking_all(db)
self.assertEqual('NONE', none.response[0])
none = controller.mail_tracking_event(db, 'open')
self.assertEqual('NONE', none.response[0])

19
mail_tracking/views/assets.xml

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<openerp>
<data>
<template id="assets_backend"
name="mail_tracking assets"
inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet"
href="/mail_tracking/static/src/css/mail_tracking.css"/>
<script type="text/javascript"
src="/mail_tracking/static/src/js/mail_tracking.js"/>
</xpath>
</template>
</data>
</openerp>

122
mail_tracking/views/mail_tracking_email_view.xml

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<openerp>
<data>
<record model="ir.ui.view" id="view_mail_tracking_email_form">
<field name="name">mail.tracking.email.form</field>
<field name="model">mail.tracking.email</field>
<field name="arch" type="xml">
<form string="MailTracking event" create="false" edit="false" delete="false">
<header>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<group>
<field name="name"/>
</group>
<group>
<group>
<field name="mail_message_id"/>
<field name="mail_id"/>
<field name="partner_id"/>
<field name="recipient"/>
<field name="sender"/>
</group>
<group>
<field name="timestamp"/>
<field name="time"/>
<field name="date"/>
</group>
</group>
<group attrs="{'invisible': [('bounce_type', '=', False)]}">
<field name="bounce_type"/>
<field name="bounce_description"/>
</group>
<group attrs="{'invisible': [('error_type', '=', False)]}">
<field name="error_smtp_server"
attrs="{'invisible': [('error_smtp_server', '=', False)]}"/>
<field name="error_type"/>
<field name="error_description"/>
</group>
<label for="tracking_event_ids"/>
<div>
<field name="tracking_event_ids">
<tree string="Tracking events" colors="grey:event_type in ('deferral');black:event_type in ('send');red:event_type in ('hard_bounce', 'soft_bounce', 'spam', 'reject');blue:event_type in ('unsub', 'click', 'open')">
<field name="time"/>
<field name="event_type"/>
<field name="ip"/>
<field name="url"/>
<field name="user_country_id" string="Country"/>
<field name="os_family" string="OS"/>
<field name="ua_family" string="User agent"/>
</tree>
</field>
</div>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_tracking_email_tree">
<field name="name">mail.tracking.email.tree</field>
<field name="model">mail.tracking.email</field>
<field name="arch" type="xml">
<tree string="MailTracking emails" create="false" edit="false" delete="false"
colors="grey:state in (False, 'deferred');black:state in ('sent', 'delivered');green:state in ('opened');red:state in ('rejected', 'spam', 'bounced', 'soft-bounced');blue:state in ('unsub')">
<field name="time"/>
<field name="date" invisible="1"/>
<field name="name"/>
<field name="sender" string="Sender"/>
<field name="recipient" string="Recipient"/>
<field name="state"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_tracking_email_search">
<field name="name">mail.tracking.email.search</field>
<field name="model">mail.tracking.email</field>
<field name="arch" type="xml">
<search string="MailTracking email search">
<field name="display_name" string="Email"
filter_domain="['|', ('sender', 'ilike', self), ('recipient', 'ilike', self)]"/>
<field name="sender" string="Sender"/>
<field name="recipient" string="Sender"/>
<field name="name" string="Subject"/>
<field name="time" string="Time"/>
<field name="date" string="Date"/>
<filter name="sent" string="Sent" domain="[('state', 'in', ('sent',))]"/>
<filter name="deferred" string="Deferred" domain="[('state', '=', 'deferred')]"/>
<filter name="delivered" string="Delivered" domain="[('state', 'in', ('delivered', 'opened'))]"/>
<filter name="unsub" string="Unsubscribed" domain="[('state', '=', 'unsub')]"/>
<filter name="exception" string="Failed"
domain="[('state', 'in', ('error', 'rejected', 'spam', 'bounced', 'soft-bounced'))]"/>
<separator/>
<group expand="0" string="Group By">
<filter string="State" domain="[]" context="{'group_by': 'state'}"/>
<filter string="Subject" domain="[]" context="{'group_by': 'name'}"/>
<filter string="Sender" domain="[]" context="{'group_by': 'sender'}"/>
<filter string="Month" domain="[]" context="{'group_by': 'date'}"/>
</group>
</search>
</field>
</record>
<record id="action_view_mail_tracking_email" model="ir.actions.act_window">
<field name="name">MailTracking emails</field>
<field name="res_model">mail.tracking.email</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_mail_tracking_email_search"/>
</record>
<!-- Add menu entry in Settings/Email -->
<menuitem name="Tracking emails" id="menu_mail_tracking_email"
parent="base.menu_email"
action="action_view_mail_tracking_email"/>
</data>
</openerp>

125
mail_tracking/views/mail_tracking_event_view.xml

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<openerp>
<data>
<record model="ir.ui.view" id="view_mail_tracking_event_form">
<field name="name">mail.tracking.event.form</field>
<field name="model">mail.tracking.event</field>
<field name="arch" type="xml">
<form string="MailTracking event" create="false" edit="false" delete="false">
<sheet>
<group>
<group>
<field name="tracking_email_id"/>
<field name="recipient"/>
<field name="event_type"/>
</group>
<group>
<field name="timestamp"/>
<field name="time"/>
<field name="date"/>
</group>
</group>
<group attrs="{'invisible': [('event_type', 'not in', ('sent',))]}">
<field name="smtp_server"/>
</group>
<group attrs="{'invisible': [('event_type', 'not in', ('open', 'click'))]}">
<field name="url"/>
</group>
<group attrs="{'invisible': [('event_type', 'not in', ('open', 'click'))]}">
<group>
<field name="mobile"/>
<field name="ip"/>
<field name="user_country_id"/>
</group>
<group>
<field name="user_agent"/>
<field name="ua_family"/>
<field name="ua_type"/>
<field name="os_family"/>
</group>
</group>
<group string="Error"
attrs="{'invisible': [('error_type', '=', False)]}">
<field name="error_type"/>
<field name="error_description"/>
<field name="error_details"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_tracking_event_tree">
<field name="name">mail.tracking.event.tree</field>
<field name="model">mail.tracking.event</field>
<field name="arch" type="xml">
<tree string="MailTracking events" create="false" edit="false" delete="false"
colors="grey:event_type in ('deferral',);black:event_type in ('sent', 'delivered');red:event_type in ('hard_bounce', 'soft_bounce', 'spam', 'reject');blue:event_type in ('unsub', 'click', 'open')">
<field name="time"/>
<field name="tracking_email_id"/>
<field name="recipient"/>
<field name="event_type"/>
<field name="date" invisible="1"/>
<field name="ip"/>
<field name="url"/>
<field name="user_country_id" string="Country"/>
<field name="os_family" string="OS"/>
<field name="ua_family" string="User agent"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_tracking_event_search">
<field name="name">mail.tracking.event.search</field>
<field name="model">mail.tracking.event</field>
<field name="arch" type="xml">
<search string="MailTracking event search">
<field name="tracking_email_id" string="Message"
filter_domain="[('tracking_email_id', 'ilike', self)]"/>
<field name="recipient" string="Recipient"/>
<field name="time" string="Time"/>
<field name="date" string="Date"/>
<field name="ip" string="IP"/>
<field name="url" string="URL"/>
<filter name="sent" string="Sent" domain="[('event_type', '=', 'sent')]"/>
<filter name="delivered" string="Delivered" domain="[('event_type', '=', 'delivered')]"/>
<filter name="click" string="Click" domain="[('event_type', '=', 'click')]"/>
<filter name="open" string="Open" domain="[('event_type', '=', 'open')]"/>
<filter name="unsub" string="Unsubscribe" domain="[('event_type', '=', 'unsub')]"/>
<filter name="bounce" string="Bounce"
domain="[('event_type', 'in', ('hard_bounce', 'soft_bounce'))]"/>
<filter name="exception" string="Failed"
domain="[('event_type', 'in', ('reject', 'spam'))]"/>
<separator/>
<group expand="0" string="Group By">
<filter string="Type" domain="[]" context="{'group_by': 'event_type'}"/>
<filter string="Message" domain="[]" context="{'group_by': 'tracking_email_id'}"/>
<filter string="OS" domain="[('os_family', '!=', False)]" context="{'group_by': 'os_family'}"/>
<filter string="User agent" domain="[('ua_family', '!=', False)]" context="{'group_by': 'ua_family'}"/>
<filter string="User agent type" domain="[('ua_type', '!=', False)]" context="{'group_by': 'ua_type'}"/>
<filter string="Country" domain="[('user_country_id', '!=', False)]" context="{'group_by': 'user_country_id'}"/>
<filter string="Month" domain="[]" context="{'group_by': 'date'}"/>
</group>
</search>
</field>
</record>
<record id="action_view_mail_tracking_event" model="ir.actions.act_window">
<field name="name">MailTracking events</field>
<field name="res_model">mail.tracking.event</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_mail_tracking_event_search"/>
</record>
<!-- Add menu entry in Settings/Email -->
<menuitem name="Tracking events" id="menu_mail_tracking_event"
parent="base.menu_email"
action="action_view_mail_tracking_event"/>
</data>
</openerp>

33
mail_tracking/views/res_partner_view.xml

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<openerp>
<data>
<record model="ir.ui.view" id="view_partner_form">
<field name="name">Partner Form with tracking emails</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<div class="oe_right oe_button_box" position="inside">
<button name="%(mail_tracking.action_view_mail_tracking_email)d"
context="{'search_default_recipient': email,
'default_recipient': email}"
type="action"
class="oe_stat_button oe_inline"
icon="fa-envelope-o"
attrs="{'invisible': [('email', '=', False)]}">
<field name="tracking_emails_count"
widget="statinfo"
string="Tracking emails"/>
</button>
</div>
<field name="email" position="after">
<field name="email_score" widget="progressbar"
attrs="{'invisible': [('email', '=', False)]}"/>
</field>
</field>
</record>
</data>
</openerp>
Loading…
Cancel
Save