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.

198 lines
7.2 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
  4. import hashlib
  5. import hmac
  6. from datetime import datetime
  7. from openerp import models, api, fields
  8. import logging
  9. _logger = logging.getLogger(__name__)
  10. class MailTrackingEmail(models.Model):
  11. _inherit = "mail.tracking.email"
  12. def _country_search(self, country_code):
  13. country = False
  14. if country_code:
  15. country = self.env['res.country'].search([
  16. ('code', '=', country_code.upper()),
  17. ])
  18. if country:
  19. return country.id
  20. return False
  21. @property
  22. def _mailgun_mandatory_fields(self):
  23. return ('event', 'timestamp', 'token', 'signature',
  24. 'tracking_email_id', 'odoo_db')
  25. @property
  26. def _mailgun_event_type_mapping(self):
  27. return {
  28. # Mailgun event type: tracking event type
  29. 'delivered': 'delivered',
  30. 'opened': 'open',
  31. 'clicked': 'click',
  32. 'unsubscribed': 'unsub',
  33. 'complained': 'spam',
  34. 'bounced': 'hard_bounce',
  35. 'dropped': 'reject',
  36. }
  37. @property
  38. def _mailgun_supported_event_types(self):
  39. return self._mailgun_event_type_mapping.keys()
  40. def _mailgun_event_type_verify(self, event):
  41. event = event or {}
  42. mailgun_event_type = event.get('event')
  43. if mailgun_event_type not in self._mailgun_supported_event_types:
  44. _logger.info("Mailgun: event type '%s' not supported",
  45. mailgun_event_type)
  46. return False
  47. # OK, event type is valid
  48. return True
  49. def _mailgun_signature(self, api_key, timestamp, token):
  50. return hmac.new(
  51. key=str(api_key),
  52. msg='{}{}'.format(str(timestamp), str(token)),
  53. digestmod=hashlib.sha256).hexdigest()
  54. def _mailgun_signature_verify(self, event):
  55. event = event or {}
  56. api_key = self.env['ir.config_parameter'].get_param('mailgun.apikey')
  57. if not api_key:
  58. _logger.info("No Mailgun api key configured. "
  59. "Please add 'mailgun.apikey' to System parameters "
  60. "to enable Mailgun authentication webhoook requests. "
  61. "More info at: "
  62. "https://documentation.mailgun.com/user_manual.html"
  63. "#webhooks")
  64. else:
  65. timestamp = event.get('timestamp')
  66. token = event.get('token')
  67. signature = event.get('signature')
  68. event_digest = self._mailgun_signature(api_key, timestamp, token)
  69. if signature != event_digest:
  70. _logger.error("Mailgun: Invalid signature '%s' != '%s'",
  71. signature, event_digest)
  72. return False
  73. # OK, signature is valid
  74. return True
  75. def _db_verify(self, event):
  76. event = event or {}
  77. odoo_db = event.get('odoo_db')
  78. current_db = self.env.cr.dbname
  79. if odoo_db != current_db:
  80. _logger.info("Mailgun: Database '%s' is not the current database",
  81. odoo_db)
  82. return False
  83. # OK, DB is current
  84. return True
  85. def _mailgun_metadata(self, mailgun_event_type, event, metadata):
  86. # Get Mailgun timestamp when found
  87. ts = event.get('timestamp', False)
  88. try:
  89. ts = float(ts)
  90. except:
  91. ts = False
  92. if ts:
  93. dt = datetime.utcfromtimestamp(ts)
  94. metadata.update({
  95. 'timestamp': ts,
  96. 'time': fields.Datetime.to_string(dt),
  97. 'date': fields.Date.to_string(dt),
  98. })
  99. # Common field mapping
  100. mapping = {
  101. 'recipient': 'recipient',
  102. 'ip': 'ip',
  103. 'user_agent': 'user-agent',
  104. 'os_family': 'client-os',
  105. 'ua_family': 'client-name',
  106. 'ua_type': 'client-type',
  107. 'url': 'url',
  108. }
  109. for k, v in mapping.iteritems():
  110. if event.get(v, False):
  111. metadata[k] = event[v]
  112. # Special field mapping
  113. metadata.update({
  114. 'mobile': event.get('device-type') in ('mobile', 'tablet'),
  115. 'user_country_id': self._country_search(
  116. event.get('country', False)),
  117. })
  118. # Mapping for special events
  119. if mailgun_event_type == 'bounced':
  120. metadata.update({
  121. 'error_type': event.get('code', False),
  122. 'error_description': event.get('error', False),
  123. 'error_details': event.get('notification', False),
  124. })
  125. elif mailgun_event_type == 'dropped':
  126. metadata.update({
  127. 'error_type': event.get('reason', False),
  128. 'error_description': event.get('code', False),
  129. 'error_details': event.get('description', False),
  130. })
  131. elif mailgun_event_type == 'complained':
  132. metadata.update({
  133. 'error_type': 'spam',
  134. 'error_description':
  135. "Recipient '%s' mark this email as spam" %
  136. event.get('recipient', False),
  137. })
  138. return metadata
  139. def _mailgun_tracking_get(self, event):
  140. tracking = False
  141. tracking_email_id = event.get('tracking_email_id', False)
  142. if tracking_email_id and tracking_email_id.isdigit():
  143. tracking = self.search([('id', '=', tracking_email_id)], limit=1)
  144. return tracking
  145. def _event_is_from_mailgun(self, event):
  146. event = event or {}
  147. return all([k in event for k in self._mailgun_mandatory_fields])
  148. @api.model
  149. def event_process(self, request, post, metadata, event_type=None):
  150. res = super(MailTrackingEmail, self).event_process(
  151. request, post, metadata, event_type=event_type)
  152. if res == 'NONE' and self._event_is_from_mailgun(post):
  153. if not self._mailgun_signature_verify(post):
  154. res = 'ERROR: Signature'
  155. elif not self._mailgun_event_type_verify(post):
  156. res = 'ERROR: Event type not supported'
  157. elif not self._db_verify(post):
  158. res = 'ERROR: Invalid DB'
  159. else:
  160. res = 'OK'
  161. if res == 'OK':
  162. mailgun_event_type = post.get('event')
  163. mapped_event_type = self._mailgun_event_type_mapping.get(
  164. mailgun_event_type) or event_type
  165. if not mapped_event_type: # pragma: no cover
  166. res = 'ERROR: Bad event'
  167. tracking = self._mailgun_tracking_get(post)
  168. if not tracking:
  169. res = 'ERROR: Tracking not found'
  170. if res == 'OK':
  171. # Complete metadata with mailgun event info
  172. metadata = self._mailgun_metadata(
  173. mailgun_event_type, post, metadata)
  174. # Create event
  175. tracking.event_create(mapped_event_type, metadata)
  176. if res != 'NONE':
  177. if event_type:
  178. _logger.info(
  179. "Mailgun: event '%s' process '%s'", event_type, res)
  180. else:
  181. _logger.info("Mailgun: event process '%s'", res)
  182. return res