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.

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