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.

171 lines
6.4 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
  4. import logging
  5. from datetime import datetime
  6. from werkzeug.useragents import UserAgent
  7. from odoo import models, fields, api
  8. _logger = logging.getLogger(__name__)
  9. class MailTrackingEmail(models.Model):
  10. """ Count the user clicks on links inside e-mails sent.
  11. Add tracking methods to process Sendgrid Notifications
  12. """
  13. _inherit = 'mail.tracking.email'
  14. click_count = fields.Integer(
  15. compute='_compute_clicks', store=True, readonly=True)
  16. @api.depends('tracking_event_ids')
  17. def _compute_clicks(self):
  18. for mail in self:
  19. mail.click_count = self.env['mail.tracking.event'].search_count([
  20. ('event_type', '=', 'click'),
  21. ('tracking_email_id', '=', mail.id)
  22. ])
  23. @property
  24. def _sendgrid_mandatory_fields(self):
  25. return ('event', 'timestamp', 'odoo_id', 'odoo_db')
  26. @property
  27. def _sendgrid_event_type_mapping(self):
  28. return {
  29. # Sendgrid event type: tracking event type
  30. 'bounce': 'hard_bounce',
  31. 'click': 'click',
  32. 'deferred': 'deferral',
  33. 'delivered': 'delivered',
  34. 'dropped': 'reject',
  35. 'group_unsubscribe': 'unsub',
  36. 'open': 'open',
  37. 'processed': 'sent',
  38. 'spamreport': 'spam',
  39. 'unsubscribe': 'unsub',
  40. }
  41. def _sendgrid_event_type_verify(self, event):
  42. event = event or {}
  43. sendgrid_event_type = event.get('event')
  44. if sendgrid_event_type not in self._sendgrid_event_type_mapping:
  45. _logger.error("Sendgrid: event type '%s' not supported",
  46. sendgrid_event_type)
  47. return False
  48. # OK, event type is valid
  49. return True
  50. def _sendgrid_db_verify(self, event):
  51. event = event or {}
  52. odoo_db = event.get('odoo_db')
  53. current_db = self.env.cr.dbname
  54. if odoo_db != current_db:
  55. _logger.error("Sendgrid: Database '%s' is not the current "
  56. "database",
  57. odoo_db)
  58. return False
  59. # OK, DB is current
  60. return True
  61. def _sendgrid_metadata(self, sendgrid_event_type, event, metadata):
  62. # Get sendgrid timestamp when found
  63. ts = event.get('timestamp')
  64. try:
  65. ts = float(ts)
  66. except ValueError:
  67. ts = False
  68. if ts:
  69. dt = datetime.utcfromtimestamp(ts)
  70. metadata.update({
  71. 'timestamp': ts,
  72. 'time': fields.Datetime.to_string(dt),
  73. 'date': fields.Date.to_string(dt),
  74. })
  75. # Common field mapping (sendgrid_field: odoo_field)
  76. mapping = {
  77. 'email': 'recipient',
  78. 'ip': 'ip',
  79. 'url': 'url',
  80. }
  81. for k, v in mapping.iteritems():
  82. if event.get(k, False):
  83. metadata[v] = event[k]
  84. # Special field mapping
  85. if event.get('useragent'):
  86. user_agent = UserAgent(event['useragent'])
  87. metadata.update({
  88. 'user_agent': user_agent.string,
  89. 'os_family': user_agent.platform,
  90. 'ua_family': user_agent.browser,
  91. 'mobile': user_agent.platform in [
  92. 'android', 'iphone', 'ipad']
  93. })
  94. # Mapping for special events
  95. if sendgrid_event_type == 'bounce':
  96. metadata.update({
  97. 'error_type': event.get('type', False),
  98. 'bounce_type': event.get('type', False),
  99. 'error_description': event.get('reason', False),
  100. 'bounce_description': event.get('reason', False),
  101. 'error_details': event.get('status', False),
  102. })
  103. elif sendgrid_event_type == 'dropped':
  104. metadata.update({
  105. 'error_type': event.get('reason', False),
  106. })
  107. return metadata
  108. def _sendgrid_tracking_get(self, event):
  109. tracking = False
  110. message_id = event.get('odoo_id', False)
  111. if message_id:
  112. tracking = self.search([
  113. ('mail_id.message_id', '=', message_id),
  114. ('recipient', '=ilike', event.get('email'))], limit=1)
  115. return tracking
  116. def _event_is_from_sendgrid(self, event):
  117. event = event or {}
  118. return all([k in event for k in self._sendgrid_mandatory_fields])
  119. @api.model
  120. def event_process(self, request, post, metadata, event_type=None):
  121. res = super(MailTrackingEmail, self).event_process(
  122. request, post, metadata, event_type=event_type)
  123. is_json = hasattr(request, 'jsonrequest') and isinstance(
  124. request.jsonrequest, list)
  125. if res == 'NONE' and is_json:
  126. for event in request.jsonrequest:
  127. if self._event_is_from_sendgrid(event):
  128. if not self._sendgrid_event_type_verify(event):
  129. res = 'ERROR: Event type not supported'
  130. elif not self._sendgrid_db_verify(event):
  131. res = 'ERROR: Invalid DB'
  132. else:
  133. res = 'OK'
  134. if res == 'OK':
  135. sendgrid_event_type = event.get('event')
  136. mapped_event_type = self._sendgrid_event_type_mapping.get(
  137. sendgrid_event_type) or event_type
  138. if not mapped_event_type:
  139. res = 'ERROR: Bad event'
  140. tracking = self._sendgrid_tracking_get(event)
  141. if tracking:
  142. # Complete metadata with sendgrid event info
  143. metadata = self._sendgrid_metadata(
  144. sendgrid_event_type, event, metadata)
  145. # Create event
  146. tracking.event_create(mapped_event_type, metadata)
  147. else:
  148. res = 'ERROR: Tracking not found'
  149. if res != 'NONE':
  150. if event_type:
  151. _logger.info(
  152. "sendgrid: event '%s' process '%s'",
  153. event_type, res)
  154. else:
  155. _logger.info("sendgrid: event process '%s'", res)
  156. return res