You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

305 lines
12 KiB

  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. name = fields.Char(string="Subject", readonly=True, index=True)
  20. display_name = fields.Char(
  21. string="Display name", readonly=True, store=True,
  22. compute="_compute_display_name")
  23. timestamp = fields.Float(
  24. string='UTC timestamp', readonly=True,
  25. digits=dp.get_precision('MailTracking Timestamp'))
  26. time = fields.Datetime(string="Time", readonly=True)
  27. date = fields.Date(
  28. string="Date", readonly=True, compute="_compute_date", store=True)
  29. mail_message_id = fields.Many2one(
  30. string="Message", comodel_name='mail.message', readonly=True)
  31. mail_id = fields.Many2one(
  32. string="Email", comodel_name='mail.mail', readonly=True)
  33. partner_id = fields.Many2one(
  34. string="Partner", comodel_name='res.partner', readonly=True)
  35. recipient = fields.Char(string='Recipient email', readonly=True)
  36. recipient_address = fields.Char(
  37. string='Recipient email address', readonly=True, store=True,
  38. compute='_compute_recipient_address')
  39. sender = fields.Char(string='Sender email', readonly=True)
  40. state = fields.Selection([
  41. ('error', 'Error'),
  42. ('deferred', 'Deferred'),
  43. ('sent', 'Sent'),
  44. ('delivered', 'Delivered'),
  45. ('opened', 'Open'),
  46. ('rejected', 'Rejected'),
  47. ('spam', 'Spam'),
  48. ('unsub', 'Unsubscribed'),
  49. ('bounced', 'Bounced'),
  50. ('soft-bounced', 'Soft bounced'),
  51. ], string='State', index=True, readonly=True, default=False,
  52. help=" * The 'Error' status indicates that there was an error "
  53. "when trying to sent the email, for example, "
  54. "'No valid recipient'\n"
  55. " * The 'Sent' status indicates that message was succesfully "
  56. "sent via outgoing email server (SMTP).\n"
  57. " * The 'Delivered' status indicates that message was "
  58. "succesfully delivered to recipient Mail Exchange (MX) server.\n"
  59. " * The 'Open' status indicates that message was opened or "
  60. "clicked by recipient.\n"
  61. " * The 'Rejected' status indicates that recipient email "
  62. "address is blacklisted by outgoing email server (SMTP). "
  63. "It is recomended to delete this email address.\n"
  64. " * The 'Spam' status indicates that outgoing email "
  65. "server (SMTP) consider this message as spam.\n"
  66. " * The 'Unsubscribed' status indicates that recipient has "
  67. "requested to be unsubscribed from this message.\n"
  68. " * The 'Bounced' status indicates that message was bounced "
  69. "by recipient Mail Exchange (MX) server.\n"
  70. " * The 'Soft bounced' status indicates that message was soft "
  71. "bounced by recipient Mail Exchange (MX) server.\n")
  72. error_smtp_server = fields.Char(string='Error SMTP server', readonly=True)
  73. error_type = fields.Char(string='Error type', readonly=True)
  74. error_description = fields.Char(
  75. string='Error description', readonly=True)
  76. bounce_type = fields.Char(string='Bounce type', readonly=True)
  77. bounce_description = fields.Char(
  78. string='Bounce description', readonly=True)
  79. tracking_event_ids = fields.One2many(
  80. string="Tracking events", comodel_name='mail.tracking.event',
  81. inverse_name='tracking_email_id', readonly=True)
  82. @api.model
  83. def tracking_ids_recalculate(self, model, email_field, tracking_field,
  84. email, new_tracking=None):
  85. objects = self.env[model].search([
  86. (email_field, '=ilike', email),
  87. ])
  88. for obj in objects:
  89. trackings = obj[tracking_field]
  90. if new_tracking:
  91. trackings |= new_tracking
  92. trackings = trackings._email_score_tracking_filter()
  93. if set(obj[tracking_field].ids) != set(trackings.ids):
  94. if trackings:
  95. obj.write({
  96. tracking_field: [(6, False, trackings.ids)]
  97. })
  98. else:
  99. obj.write({
  100. tracking_field: [(5, False, False)]
  101. })
  102. return objects
  103. @api.model
  104. def _tracking_ids_to_write(self, email):
  105. trackings = self.env['mail.tracking.email'].search([
  106. ('recipient_address', '=ilike', email)
  107. ])
  108. trackings = trackings._email_score_tracking_filter()
  109. if trackings:
  110. return [(6, False, trackings.ids)]
  111. else:
  112. return [(5, False, False)]
  113. @api.multi
  114. def _email_score_tracking_filter(self):
  115. """Default email score filter for tracking emails"""
  116. # Consider only last 10 tracking emails
  117. return self.sorted(key=lambda r: r.time, reverse=True)[:10]
  118. @api.multi
  119. def email_score(self):
  120. """Default email score algorimth"""
  121. score = 50.0
  122. trackings = self._email_score_tracking_filter()
  123. for tracking in trackings:
  124. if tracking.state in ('error',):
  125. score -= 50.0
  126. elif tracking.state in ('rejected', 'spam', 'bounced'):
  127. score -= 25.0
  128. elif tracking.state in ('soft-bounced', 'unsub'):
  129. score -= 10.0
  130. elif tracking.state in ('delivered',):
  131. score += 5.0
  132. elif tracking.state in ('opened',):
  133. score += 10.0
  134. if score > 100.0:
  135. score = 100.0
  136. return score
  137. @api.multi
  138. @api.depends('recipient')
  139. def _compute_recipient_address(self):
  140. for email in self:
  141. matches = re.search(r'<(.*@.*)>', email.recipient)
  142. if matches:
  143. email.recipient_address = matches.group(1)
  144. else:
  145. email.recipient_address = email.recipient
  146. @api.multi
  147. @api.depends('name', 'recipient')
  148. def _compute_display_name(self):
  149. for email in self:
  150. parts = [email.name]
  151. if email.recipient:
  152. parts.append(email.recipient)
  153. email.display_name = ' - '.join(parts)
  154. @api.multi
  155. @api.depends('time')
  156. def _compute_date(self):
  157. for email in self:
  158. email.date = fields.Date.to_string(
  159. fields.Date.from_string(email.time))
  160. @api.model
  161. def create(self, vals):
  162. tracking = super(MailTrackingEmail, self).create(vals)
  163. self.tracking_ids_recalculate(
  164. 'res.partner', 'email', 'tracking_email_ids',
  165. tracking.recipient_address, new_tracking=tracking)
  166. return tracking
  167. def _get_mail_tracking_img(self):
  168. base_url = self.env['ir.config_parameter'].get_param('web.base.url')
  169. path_url = (
  170. 'mail/tracking/open/%(db)s/%(tracking_email_id)s/blank.gif' % {
  171. 'db': self.env.cr.dbname,
  172. 'tracking_email_id': self.id,
  173. })
  174. track_url = urlparse.urljoin(base_url, path_url)
  175. return (
  176. '<img src="%(url)s" alt="" '
  177. 'data-odoo-tracking-email="%(tracking_email_id)s"/>' % {
  178. 'url': track_url,
  179. 'tracking_email_id': self.id,
  180. })
  181. @api.multi
  182. def smtp_error(self, mail_server, smtp_server, exception):
  183. self.sudo().write({
  184. 'error_smtp_server': tools.ustr(smtp_server),
  185. 'error_type': exception.__class__.__name__,
  186. 'error_description': tools.ustr(exception),
  187. 'state': 'error',
  188. })
  189. return True
  190. @api.multi
  191. def tracking_img_add(self, email):
  192. self.ensure_one()
  193. tracking_url = self._get_mail_tracking_img()
  194. if tracking_url:
  195. body = tools.append_content_to_html(
  196. email.get('body', ''), tracking_url, plaintext=False,
  197. container_tag='div')
  198. email['body'] = body
  199. return email
  200. def _message_partners_check(self, message, message_id):
  201. mail_message = self.mail_message_id
  202. partners = mail_message.notified_partner_ids | mail_message.partner_ids
  203. if (self.partner_id and self.partner_id not in partners):
  204. # If mail_message haven't tracking partner, then
  205. # add it in order to see his trackking status in chatter
  206. if mail_message.subtype_id:
  207. mail_message.sudo().write({
  208. 'notified_partner_ids': [(4, self.partner_id.id)],
  209. })
  210. else:
  211. mail_message.sudo().write({
  212. 'partner_ids': [(4, self.partner_id.id)],
  213. })
  214. return True
  215. @api.multi
  216. def _tracking_sent_prepare(self, mail_server, smtp_server, message,
  217. message_id):
  218. self.ensure_one()
  219. ts = time.time()
  220. dt = datetime.utcfromtimestamp(ts)
  221. self._message_partners_check(message, message_id)
  222. self.sudo().write({'state': 'sent'})
  223. return {
  224. 'recipient': message['To'],
  225. 'timestamp': '%.6f' % ts,
  226. 'time': fields.Datetime.to_string(dt),
  227. 'tracking_email_id': self.id,
  228. 'event_type': 'sent',
  229. 'smtp_server': smtp_server,
  230. }
  231. def _event_prepare(self, event_type, metadata):
  232. self.ensure_one()
  233. m_event = self.env['mail.tracking.event']
  234. method = getattr(m_event, 'process_' + event_type, None)
  235. if method and hasattr(method, '__call__'):
  236. return method(self, metadata)
  237. else: # pragma: no cover
  238. _logger.info('Unknown event type: %s' % event_type)
  239. return False
  240. def _concurrent_events(self, event_type, metadata):
  241. m_event = self.env['mail.tracking.event']
  242. self.ensure_one()
  243. concurrent_event_ids = False
  244. if event_type in {'open', 'click'}:
  245. ts = metadata.get('timestamp', time.time())
  246. delta = EVENT_OPEN_DELTA if event_type == 'open' \
  247. else EVENT_CLICK_DELTA
  248. domain = [
  249. ('timestamp', '>=', ts - delta),
  250. ('timestamp', '<=', ts + delta),
  251. ('tracking_email_id', '=', self.id),
  252. ('event_type', '=', event_type),
  253. ]
  254. if event_type == 'click':
  255. domain.append(('url', '=', metadata.get('url', False)))
  256. concurrent_event_ids = m_event.search(domain)
  257. return concurrent_event_ids
  258. @api.multi
  259. def event_create(self, event_type, metadata):
  260. event_ids = self.env['mail.tracking.event']
  261. for tracking_email in self:
  262. other_ids = tracking_email._concurrent_events(event_type, metadata)
  263. if not other_ids:
  264. vals = tracking_email._event_prepare(event_type, metadata)
  265. if vals:
  266. event_ids += event_ids.sudo().create(vals)
  267. partners = self.tracking_ids_recalculate(
  268. 'res.partner', 'email', 'tracking_email_ids',
  269. tracking_email.recipient_address)
  270. if partners:
  271. partners.email_score_calculate()
  272. else:
  273. _logger.debug("Concurrent event '%s' discarded", event_type)
  274. return event_ids
  275. @api.model
  276. def event_process(self, request, post, metadata, event_type=None):
  277. # Generic event process hook, inherit it and
  278. # - return 'OK' if processed
  279. # - return 'NONE' if this request is not for you
  280. # - return 'ERROR' if any error
  281. return 'NONE' # pragma: no cover