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.

278 lines
10 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright - 2013-2018 Therp BV <https://therp.nl>.
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  4. import base64
  5. import logging
  6. from odoo import _, api, models, fields
  7. from odoo.exceptions import UserError, ValidationError
  8. from .. import match_algorithm
  9. _logger = logging.getLogger(__name__)
  10. class FetchmailServerFolder(models.Model):
  11. _name = 'fetchmail.server.folder'
  12. _rec_name = 'path'
  13. _order = 'sequence'
  14. def _get_match_algorithms(self):
  15. def get_all_subclasses(cls):
  16. return (cls.__subclasses__() +
  17. [subsub
  18. for sub in cls.__subclasses__()
  19. for subsub in get_all_subclasses(sub)])
  20. return dict([(cls.__name__, cls)
  21. for cls in get_all_subclasses(
  22. match_algorithm.base.Base)])
  23. def _get_match_algorithms_sel(self):
  24. algorithms = []
  25. for cls in self._get_match_algorithms().itervalues():
  26. algorithms.append((cls.__name__, cls.name))
  27. algorithms.sort()
  28. return algorithms
  29. server_id = fields.Many2one('fetchmail.server', 'Server')
  30. sequence = fields.Integer('Sequence')
  31. state = fields.Selection([
  32. ('draft', 'Not Confirmed'),
  33. ('done', 'Confirmed')],
  34. string='Status',
  35. readonly=True,
  36. required=True,
  37. copy=False,
  38. default='draft')
  39. path = fields.Char(
  40. 'Path',
  41. required=True,
  42. help="The path to your mail folder."
  43. " Typically would be something like 'INBOX.myfolder'")
  44. model_id = fields.Many2one(
  45. 'ir.model', 'Model', required=True,
  46. help='The model to attach emails to')
  47. model_field = fields.Char(
  48. 'Field (model)',
  49. help='The field in your model that contains the field to match '
  50. 'against.\n'
  51. 'Examples:\n'
  52. "'email' if your model is res.partner, or "
  53. "'partner_id.email' if you're matching sale orders")
  54. model_order = fields.Char(
  55. 'Order (model)',
  56. help='Field(s) to order by, this mostly useful in conjunction '
  57. "with 'Use 1st match'")
  58. match_algorithm = fields.Selection(
  59. _get_match_algorithms_sel,
  60. 'Match algorithm', required=True,
  61. help='The algorithm used to determine which object an email matches.')
  62. mail_field = fields.Char(
  63. 'Field (email)',
  64. help='The field in the email used for matching. Typically '
  65. "this is 'to' or 'from'")
  66. delete_matching = fields.Boolean(
  67. 'Delete matches',
  68. help='Delete matched emails from server')
  69. flag_nonmatching = fields.Boolean(
  70. 'Flag nonmatching',
  71. default=True,
  72. help="Flag emails in the server that don't match any object in Odoo")
  73. match_first = fields.Boolean(
  74. 'Use 1st match',
  75. help='If there are multiple matches, use the first one. If '
  76. 'not checked, multiple matches count as no match at all')
  77. domain = fields.Char(
  78. 'Domain',
  79. help='Fill in a search filter to narrow down objects to match')
  80. msg_state = fields.Selection(
  81. selection=[('sent', 'Sent'), ('received', 'Received')],
  82. string='Message state',
  83. default='received',
  84. help='The state messages fetched from this folder should be '
  85. 'assigned in Odoo')
  86. active = fields.Boolean('Active', default=True)
  87. @api.multi
  88. def get_algorithm(self):
  89. return self._get_match_algorithms()[self.match_algorithm]()
  90. @api.multi
  91. def button_confirm_folder(self):
  92. for this in self:
  93. this.write({'state': 'draft'})
  94. if not this.active:
  95. continue
  96. connection = this.server_id.connect()
  97. connection.select()
  98. if connection.select(this.path)[0] != 'OK':
  99. raise ValidationError(
  100. _('Invalid folder %s!') % this.path)
  101. connection.close()
  102. this.write({'state': 'done'})
  103. @api.multi
  104. def button_attach_mail_manually(self):
  105. self.ensure_one()
  106. return {
  107. 'type': 'ir.actions.act_window',
  108. 'res_model': 'fetchmail.attach.mail.manually',
  109. 'target': 'new',
  110. 'context': dict(self.env.context, folder_id=self.id),
  111. 'view_type': 'form',
  112. 'view_mode': 'form'}
  113. @api.multi
  114. def set_draft(self):
  115. self.write({'state': 'draft'})
  116. return True
  117. @api.multi
  118. def get_msgids(self, connection, criteria):
  119. """Return imap ids of messages to process"""
  120. self.ensure_one()
  121. server = self.server_id
  122. _logger.info(
  123. 'start checking for emails in folder %s on server %s',
  124. self.path, server.name)
  125. if connection.select(self.path)[0] != 'OK':
  126. raise UserError(_(
  127. "Could not open mailbox %s on %s") %
  128. (self.path, server.name))
  129. result, msgids = connection.search(None, criteria)
  130. if result != 'OK':
  131. raise UserError(_(
  132. "Could not search mailbox %s on %s") %
  133. (self.path, server.name))
  134. _logger.info(
  135. 'finished checking for emails in %s on server %s',
  136. self.path, server.name)
  137. return msgids
  138. @api.multi
  139. def fetch_msg(self, connection, msgid):
  140. """Select a single message from a folder."""
  141. self.ensure_one()
  142. server = self.server_id
  143. result, msgdata = connection.fetch(msgid, '(RFC822)')
  144. if result != 'OK':
  145. raise UserError(_(
  146. "Could not fetch %s in %s on %s") %
  147. (msgid, self.path, server.server))
  148. message_org = msgdata[0][1] # rfc822 message source
  149. mail_message = self.env['mail.thread'].message_parse(
  150. message_org, save_original=server.original)
  151. return (mail_message, message_org)
  152. @api.multi
  153. def retrieve_imap_folder(self, connection):
  154. """Retrieve all mails for one IMAP folder."""
  155. self.ensure_one()
  156. msgids = self.get_msgids(connection, 'UNDELETED')
  157. match_algorithm = self.get_algorithm()
  158. for msgid in msgids[0].split():
  159. # We will accept exceptions for single messages
  160. try:
  161. self.env.cr.execute('savepoint apply_matching')
  162. self.apply_matching(connection, msgid, match_algorithm)
  163. self.env.cr.execute('release savepoint apply_matching')
  164. except Exception:
  165. self.env.cr.execute('rollback to savepoint apply_matching')
  166. _logger.exception(
  167. "Failed to fetch mail %s from %s",
  168. msgid, self.server_id.name)
  169. @api.multi
  170. def fetch_mail(self):
  171. """Retrieve all mails for IMAP folders.
  172. We will use a separate connection for each folder.
  173. """
  174. for this in self:
  175. if not this.active or this.state != 'done':
  176. continue
  177. connection = None
  178. try:
  179. # New connection per folder
  180. connection = this.server_id.connect()
  181. this.retrieve_imap_folder(connection)
  182. connection.close()
  183. except Exception:
  184. _logger.error(_(
  185. "General failure when trying to connect to %s server %s."),
  186. this.server_id.type, this.server_id.name, exc_info=True)
  187. finally:
  188. if connection:
  189. connection.logout()
  190. @api.multi
  191. def update_msg(self, connection, msgid, matched=True, flagged=False):
  192. """Update msg in imap folder depending on match and settings."""
  193. if matched:
  194. if self.delete_matching:
  195. connection.store(msgid, '+FLAGS', '\\DELETED')
  196. elif flagged and self.flag_nonmatching:
  197. connection.store(msgid, '-FLAGS', '\\FLAGGED')
  198. else:
  199. if self.flag_nonmatching:
  200. connection.store(msgid, '+FLAGS', '\\FLAGGED')
  201. @api.multi
  202. def apply_matching(self, connection, msgid, match_algorithm):
  203. """Return ids of objects matched"""
  204. self.ensure_one()
  205. mail_message, message_org = self.fetch_msg(connection, msgid)
  206. if self.env['mail.message'].search(
  207. [('message_id', '=', mail_message['message_id'])]):
  208. # Ignore mails that have been handled already
  209. return
  210. matches = match_algorithm.search_matches(self, mail_message)
  211. matched = matches and (len(matches) == 1 or self.match_first)
  212. if matched:
  213. match_algorithm.handle_match(
  214. connection,
  215. matches[0], self, mail_message,
  216. message_org, msgid)
  217. self.update_msg(connection, msgid, matched=matched)
  218. @api.multi
  219. def attach_mail(self, match_object, mail_message):
  220. """Attach mail to match_object."""
  221. self.ensure_one()
  222. partner = False
  223. model_name = self.model_id.model
  224. if model_name == 'res.partner':
  225. partner = match_object
  226. elif 'partner_id' in self.env[model_name]._fields:
  227. partner = match_object.partner_id
  228. attachments = []
  229. if self.server_id.attach and mail_message.get('attachments'):
  230. for attachment in mail_message['attachments']:
  231. # Attachment should at least have filename and data, but
  232. # might have some extra element(s)
  233. if len(attachment) < 2:
  234. continue
  235. fname, fcontent = attachment[:2]
  236. if isinstance(fcontent, unicode):
  237. fcontent = fcontent.encode('utf-8')
  238. data_attach = {
  239. 'name': fname,
  240. 'datas': base64.b64encode(str(fcontent)),
  241. 'datas_fname': fname,
  242. 'description': _('Mail attachment'),
  243. 'res_model': model_name,
  244. 'res_id': match_object.id}
  245. attachments.append(
  246. self.env['ir.attachment'].create(data_attach))
  247. self.env['mail.message'].create({
  248. 'author_id': partner and partner.id or False,
  249. 'model': model_name,
  250. 'res_id': match_object.id,
  251. 'message_type': 'email',
  252. 'body': mail_message.get('body'),
  253. 'subject': mail_message.get('subject'),
  254. 'email_from': mail_message.get('from'),
  255. 'date': mail_message.get('date'),
  256. 'message_id': mail_message.get('message_id'),
  257. 'attachment_ids': [(6, 0, [a.id for a in attachments])]})