mail_tracking_email.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. # -*- coding: utf-8 -*-
  2. # © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  4. import logging
  5. import urlparse
  6. import time
  7. import re
  8. from datetime import datetime
  9. from openerp import models, api, fields, tools
  10. import openerp.addons.decimal_precision as dp
  11. _logger = logging.getLogger(__name__)
  12. EVENT_OPEN_DELTA = 10 # seconds
  13. EVENT_CLICK_DELTA = 5 # seconds
  14. class MailTrackingEmail(models.Model):
  15. _name = "mail.tracking.email"
  16. _order = 'time desc'
  17. _rec_name = 'display_name'
  18. _description = 'MailTracking email'
  19. # This table is going to grow fast and to infinite, so we index:
  20. # - name: Search in tree view
  21. # - time: default order fields
  22. # - recipient_address: Used for email_store calculation (non-store)
  23. # - state: Search and group_by in tree view
  24. name = fields.Char(string="Asunto", readonly=True, index=True)
  25. display_name = fields.Char(
  26. string="Nombre Mostrado", readonly=True, store=True,
  27. compute="_compute_display_name")
  28. timestamp = fields.Float(
  29. string='UTC Fecha-Hora', readonly=True,
  30. digits=dp.get_precision('MailTracking Timestamp'))
  31. time = fields.Datetime(string="Hora", readonly=True, index=True)
  32. date = fields.Date(
  33. string="Fecha", readonly=True, compute="_compute_date", store=True)
  34. mail_message_id = fields.Many2one(
  35. string="Mensaje", comodel_name='mail.message', readonly=True)
  36. mail_id = fields.Many2one(
  37. string="Email", comodel_name='mail.mail', readonly=True)
  38. partner_id = fields.Many2one(
  39. string="Partner", comodel_name='res.partner', readonly=True)
  40. recipient = fields.Char(string='Recipiente email', readonly=True)
  41. recipient_address = fields.Char(
  42. string='Dirección del Recipiente del email', readonly=True, store=True,
  43. compute='_compute_recipient_address', index=True)
  44. sender = fields.Char(string='Email que envió', readonly=True)
  45. state = fields.Selection([
  46. ('error', 'Error'),
  47. ('deferred', 'Diferido'),
  48. ('sent', 'Enviado'),
  49. ('delivered', 'Entregado'),
  50. ('opened', 'Abierto'),
  51. ('rejected', 'Rechazado'),
  52. ('spam', 'Spam'),
  53. ('unsub', 'Desuscripto'),
  54. ('bounced', 'Rebotado'),
  55. ('soft-bounced', 'Rebotado Suavemente'),
  56. ], string='Estado', index=True, readonly=True, default=False,
  57. help="* El estado 'Error' indica que se produjo un error al intentar enviar el correo electrónico, por ejemplo, No destinatario válido \n"
  58. "* El estado de 'Enviados' indica que el mensaje fue enviado con éxito a través del servidor de correo saliente (SMTP). \n"
  59. "* El estado 'Entregado' indica que el mensaje fue entregado con éxito al servidor receptor de intercambio de correo (MX). \n"
  60. "* El estado de 'Abierto' indica que se abre o se hace clic por destinatario que el mensaje. \n"
  61. "* El estado 'Rechazado' indica que dirección de correo electrónico del destinatario en una lista negra por el servidor de correo saliente (SMTP). Está recomendado eliminar esta dirección de correo electrónico. \n"
  62. "* El estado de 'Spam' indica que el servidor de correo saliente (SMTP) debe tener en cuenta este mensaje como spam. \n"
  63. "* El estado de 'Desuscripto' indica que destinatario ha solicitado para anular su inscripción en este mensaje . \n"
  64. "* El estado 'rebotados' indica que el mensaje se ha rechazado por el servidor del destinatario de intercambio de correo (MX). \n"
  65. "* La 'suave rebotó' estado indica que el mensaje fue suave rebotó por el receptor de intercambio de correo (MX) del servidor. \n")
  66. error_smtp_server = fields.Char(string='Error del servidor SMTP ', readonly=True)
  67. error_type = fields.Char(string='Tipo de Error', readonly=True)
  68. error_description = fields.Char(
  69. string='Descripción del Error', readonly=True)
  70. bounce_type = fields.Char(string='Tipo de Rebote', readonly=True)
  71. bounce_description = fields.Char(
  72. string='Descripcion del Rebote', readonly=True)
  73. tracking_event_ids = fields.One2many(
  74. string="Estadisticas de Seguimientos", comodel_name='mail.tracking.event',
  75. inverse_name='tracking_email_id', readonly=True)
  76. @api.model
  77. def _email_score_tracking_filter(self, domain, order='time desc',
  78. limit=10):
  79. """Default tracking search. Ready to be inherited."""
  80. return self.search(domain, limit=limit, order=order)
  81. @api.model
  82. def email_is_bounced(self, email):
  83. return len(self._email_score_tracking_filter([
  84. ('recipient_address', '=ilike', email),
  85. ('state', 'in', ('error', 'rejected', 'spam', 'bounced')),
  86. ])) > 0
  87. @api.model
  88. def email_score_from_email(self, email):
  89. return self._email_score_tracking_filter([
  90. ('recipient_address', '=ilike', email)
  91. ]).email_score()
  92. @api.model
  93. def _email_score_weights(self):
  94. """Default email score weights. Ready to be inherited"""
  95. return {
  96. 'error': -50.0,
  97. 'rejected': -25.0,
  98. 'spam': -25.0,
  99. 'bounced': -25.0,
  100. 'soft-bounced': -10.0,
  101. 'unsub': -10.0,
  102. 'delivered': 1.0,
  103. 'opened': 5.0,
  104. }
  105. @api.multi
  106. def email_score(self):
  107. """Default email score algorimth. Ready to be inherited
  108. Must return a value beetwen 0.0 and 100.0
  109. - Bad reputation: Value between 0 and 50.0
  110. - Unknown reputation: Value 50.0
  111. - Good reputation: Value between 50.0 and 100.0
  112. """
  113. weights = self._email_score_weights()
  114. score = 50.0
  115. for tracking in self:
  116. score += weights.get(tracking.state, 0.0)
  117. if score > 100.0:
  118. score = 100.0
  119. elif score < 0.0:
  120. score = 0.0
  121. return score
  122. @api.multi
  123. @api.depends('recipient')
  124. def _compute_recipient_address(self):
  125. for email in self:
  126. matches = re.search(r'<(.*@.*)>', email.recipient)
  127. if matches:
  128. email.recipient_address = matches.group(1)
  129. else:
  130. email.recipient_address = email.recipient
  131. @api.multi
  132. @api.depends('name', 'recipient')
  133. def _compute_display_name(self):
  134. for email in self:
  135. parts = [email.name or '']
  136. if email.recipient:
  137. parts.append(email.recipient)
  138. email.display_name = ' - '.join(parts)
  139. @api.multi
  140. @api.depends('time')
  141. def _compute_date(self):
  142. for email in self:
  143. email.date = fields.Date.to_string(
  144. fields.Date.from_string(email.time))
  145. def _get_mail_tracking_img(self):
  146. m_config = self.env['ir.config_parameter']
  147. base_url = (m_config.get_param('mail_tracking.base.url') or
  148. m_config.get_param('web.base.url'))
  149. path_url = (
  150. 'mail/tracking/open/%(db)s/%(tracking_email_id)s/blank.gif' % {
  151. 'db': self.env.cr.dbname,
  152. 'tracking_email_id': self.id,
  153. })
  154. track_url = urlparse.urljoin(base_url, path_url)
  155. return (
  156. '<img src="%(url)s" alt="" '
  157. 'data-odoo-tracking-email="%(tracking_email_id)s"/>' % {
  158. 'url': track_url,
  159. 'tracking_email_id': self.id,
  160. })
  161. @api.multi
  162. def _partners_email_bounced_set(self, reason):
  163. for tracking_email in self:
  164. self.env['res.partner'].search([
  165. ('email', '=ilike', tracking_email.recipient_address)
  166. ]).email_bounced_set(tracking_email, reason)
  167. @api.multi
  168. def smtp_error(self, mail_server, smtp_server, exception):
  169. self.sudo().write({
  170. 'error_smtp_server': tools.ustr(smtp_server),
  171. 'error_type': exception.__class__.__name__,
  172. 'error_description': tools.ustr(exception),
  173. 'state': 'error',
  174. })
  175. self.sudo()._partners_email_bounced_set('error')
  176. return True
  177. @api.multi
  178. def tracking_img_add(self, email):
  179. self.ensure_one()
  180. tracking_url = self._get_mail_tracking_img()
  181. if tracking_url:
  182. body = tools.append_content_to_html(
  183. email.get('body', ''), tracking_url, plaintext=False,
  184. container_tag='div')
  185. email['body'] = body
  186. return email
  187. def _message_partners_check(self, message, message_id):
  188. mail_message = self.mail_message_id
  189. partners = mail_message.notified_partner_ids | mail_message.partner_ids
  190. if (self.partner_id and self.partner_id not in partners):
  191. # If mail_message haven't tracking partner, then
  192. # add it in order to see his tracking status in chatter
  193. if mail_message.subtype_id:
  194. mail_message.sudo().write({
  195. 'notified_partner_ids': [(4, self.partner_id.id)],
  196. })
  197. else:
  198. mail_message.sudo().write({
  199. 'partner_ids': [(4, self.partner_id.id)],
  200. })
  201. return True
  202. @api.multi
  203. def _tracking_sent_prepare(self, mail_server, smtp_server, message,
  204. message_id):
  205. self.ensure_one()
  206. ts = time.time()
  207. dt = datetime.utcfromtimestamp(ts)
  208. self._message_partners_check(message, message_id)
  209. self.sudo().write({'state': 'sent'})
  210. return {
  211. 'recipient': message['To'],
  212. 'timestamp': '%.6f' % ts,
  213. 'time': fields.Datetime.to_string(dt),
  214. 'tracking_email_id': self.id,
  215. 'event_type': 'sent',
  216. 'smtp_server': smtp_server,
  217. }
  218. def _event_prepare(self, event_type, metadata):
  219. self.ensure_one()
  220. m_event = self.env['mail.tracking.event']
  221. method = getattr(m_event, 'process_' + event_type, None)
  222. if method and hasattr(method, '__call__'):
  223. return method(self, metadata)
  224. else: # pragma: no cover
  225. _logger.info('Unknown event type: %s' % event_type)
  226. return False
  227. def _concurrent_events(self, event_type, metadata):
  228. m_event = self.env['mail.tracking.event']
  229. self.ensure_one()
  230. concurrent_event_ids = False
  231. if event_type in {'open', 'click'}:
  232. ts = metadata.get('timestamp', time.time())
  233. delta = EVENT_OPEN_DELTA if event_type == 'open' \
  234. else EVENT_CLICK_DELTA
  235. domain = [
  236. ('timestamp', '>=', ts - delta),
  237. ('timestamp', '<=', ts + delta),
  238. ('tracking_email_id', '=', self.id),
  239. ('event_type', '=', event_type),
  240. ]
  241. if event_type == 'click':
  242. domain.append(('url', '=', metadata.get('url', False)))
  243. concurrent_event_ids = m_event.search(domain)
  244. return concurrent_event_ids
  245. @api.multi
  246. def event_create(self, event_type, metadata):
  247. event_ids = self.env['mail.tracking.event']
  248. for tracking_email in self:
  249. other_ids = tracking_email._concurrent_events(event_type, metadata)
  250. if not other_ids:
  251. vals = tracking_email._event_prepare(event_type, metadata)
  252. if vals:
  253. event_ids += event_ids.sudo().create(vals)
  254. else:
  255. _logger.debug("Concurrent event '%s' discarded", event_type)
  256. if event_type in {'hard_bounce', 'spam', 'reject'}:
  257. self.sudo()._partners_email_bounced_set(event_type)
  258. return event_ids
  259. @api.model
  260. def event_process(self, request, post, metadata, event_type=None):
  261. # Generic event process hook, inherit it and
  262. # - return 'OK' if processed
  263. # - return 'NONE' if this request is not for you
  264. # - return 'ERROR' if any error
  265. return 'NONE' # pragma: no cover