mail_group_message.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import logging
  4. from odoo import _, api, fields, models
  5. from odoo.exceptions import AccessError, UserError
  6. from odoo.osv import expression
  7. from odoo.tools import email_normalize, append_content_to_html, ustr
  8. _logger = logging.getLogger(__name__)
  9. class MailGroupMessage(models.Model):
  10. """Emails belonging to a discussion group.
  11. Those are build on <mail.message> with additional information related to specific
  12. features of <mail.group> like better parent / children management and moderation.
  13. """
  14. _name = 'mail.group.message'
  15. _description = 'Mailing List Message'
  16. _rec_name = 'subject'
  17. _order = 'create_date DESC'
  18. _primary_email = 'email_from'
  19. # <mail.message> fields, can not be done with inherits because it will impact
  20. # the performance of the <mail.message> model (different cache, so the ORM will need
  21. # to do one more SQL query to be able to update the <mail.group.message> cache)
  22. attachment_ids = fields.Many2many(related='mail_message_id.attachment_ids', readonly=False)
  23. author_id = fields.Many2one(related='mail_message_id.author_id', readonly=False)
  24. email_from = fields.Char(related='mail_message_id.email_from', readonly=False)
  25. email_from_normalized = fields.Char('Normalized From', compute='_compute_email_from_normalized', store=True)
  26. body = fields.Html(related='mail_message_id.body', readonly=False)
  27. subject = fields.Char(related='mail_message_id.subject', readonly=False)
  28. # Thread
  29. mail_group_id = fields.Many2one(
  30. 'mail.group', string='Group',
  31. required=True, ondelete='cascade')
  32. mail_message_id = fields.Many2one('mail.message', 'Mail Message', required=True, ondelete='cascade', index=True, copy=False)
  33. # Parent and children
  34. group_message_parent_id = fields.Many2one(
  35. 'mail.group.message', string='Parent', store=True)
  36. group_message_child_ids = fields.One2many('mail.group.message', 'group_message_parent_id', string='Children')
  37. # Moderation
  38. author_moderation = fields.Selection([('ban', 'Banned'), ('allow', 'Whitelisted')], string='Author Moderation Status',
  39. compute='_compute_author_moderation')
  40. is_group_moderated = fields.Boolean('Is Group Moderated', related='mail_group_id.moderation')
  41. moderation_status = fields.Selection(
  42. [('pending_moderation', 'Pending Moderation'),
  43. ('accepted', 'Accepted'),
  44. ('rejected', 'Rejected')],
  45. string='Status', index=True, copy=False,
  46. required=True, default='pending_moderation')
  47. moderator_id = fields.Many2one('res.users', string='Moderated By')
  48. create_date = fields.Datetime(string='Posted')
  49. @api.depends('email_from')
  50. def _compute_email_from_normalized(self):
  51. for message in self:
  52. message.email_from_normalized = email_normalize(message.email_from)
  53. @api.depends('email_from_normalized', 'mail_group_id')
  54. def _compute_author_moderation(self):
  55. moderations = self.env['mail.group.moderation'].search([
  56. ('mail_group_id', 'in', self.mail_group_id.ids),
  57. ])
  58. all_emails = set(self.mapped('email_from_normalized'))
  59. moderations = {
  60. (moderation.mail_group_id, moderation.email): moderation.status
  61. for moderation in moderations
  62. if moderation.email in all_emails
  63. }
  64. for message in self:
  65. message.author_moderation = moderations.get((message.mail_group_id, message.email_from_normalized), False)
  66. @api.constrains('mail_message_id')
  67. def _constrains_mail_message_id(self):
  68. for message in self:
  69. if message.mail_message_id.model != 'mail.group':
  70. raise AccessError(_(
  71. 'Group message can only be linked to mail group. Current model is %s.',
  72. message.mail_message_id.model,
  73. ))
  74. if message.mail_message_id.res_id != message.mail_group_id.id:
  75. raise AccessError(_('The record of the message should be the group.'))
  76. @api.model_create_multi
  77. def create(self, values_list):
  78. for vals in values_list:
  79. if not vals.get('mail_message_id'):
  80. vals.update({
  81. 'res_id': vals.get('mail_group_id'),
  82. 'model': 'mail.group',
  83. })
  84. vals['mail_message_id'] = self.env['mail.message'].sudo().create({
  85. field: vals.pop(field)
  86. for field in self.env['mail.message']._fields
  87. if field in vals
  88. }).id
  89. return super(MailGroupMessage, self).create(values_list)
  90. def copy(self, default=None):
  91. default = dict(default or {})
  92. default['mail_message_id'] = self.mail_message_id.copy().id
  93. return super(MailGroupMessage, self).copy(default)
  94. # --------------------------------------------------
  95. # MODERATION API
  96. # --------------------------------------------------
  97. def action_moderate_accept(self):
  98. """Accept the incoming email.
  99. Will send the incoming email to all members of the group.
  100. """
  101. self._assert_moderable()
  102. self.write({
  103. 'moderation_status': 'accepted',
  104. 'moderator_id': self.env.uid,
  105. })
  106. # Send the email to the members of the group
  107. for message in self:
  108. message.mail_group_id._notify_members(message)
  109. def action_moderate_reject_with_comment(self, reject_subject, reject_comment):
  110. self._assert_moderable()
  111. if reject_subject or reject_comment:
  112. self._moderate_send_reject_email(reject_subject, reject_comment)
  113. self.action_moderate_reject()
  114. def action_moderate_reject(self):
  115. self._assert_moderable()
  116. self.write({
  117. 'moderation_status': 'rejected',
  118. 'moderator_id': self.env.uid,
  119. })
  120. def action_moderate_allow(self):
  121. self._create_moderation_rule('allow')
  122. # Accept all emails of the same authors
  123. same_author = self._get_pending_same_author_same_group()
  124. same_author.action_moderate_accept()
  125. def action_moderate_ban(self):
  126. self._create_moderation_rule('ban')
  127. # Reject all emails of the same author
  128. same_author = self._get_pending_same_author_same_group()
  129. same_author.action_moderate_reject()
  130. def action_moderate_ban_with_comment(self, ban_subject, ban_comment):
  131. self._create_moderation_rule('ban')
  132. if ban_subject or ban_comment:
  133. self._moderate_send_reject_email(ban_subject, ban_comment)
  134. # Reject all emails of the same author
  135. same_author = self._get_pending_same_author_same_group()
  136. same_author.action_moderate_reject()
  137. def _get_pending_same_author_same_group(self):
  138. """Return the pending messages of the same authors in the same groups."""
  139. return self.search(
  140. expression.AND([
  141. expression.OR([
  142. [
  143. ('mail_group_id', '=', message.mail_group_id.id),
  144. ('email_from_normalized', '=', message.email_from_normalized),
  145. ] for message in self
  146. ]),
  147. [('moderation_status', '=', 'pending_moderation')],
  148. ])
  149. )
  150. def _create_moderation_rule(self, status):
  151. """Create a moderation rule <mail.group.moderation> with the given status.
  152. Update existing moderation rule for the same email address if found,
  153. otherwise create a new rule.
  154. """
  155. if status not in ('ban', 'allow'):
  156. raise ValueError(_('Wrong status (%s)', status))
  157. for message in self:
  158. if not email_normalize(message.email_from):
  159. raise UserError(_('The email "%s" is not valid.', message.email_from))
  160. existing_moderation = self.env['mail.group.moderation'].search(
  161. expression.OR([
  162. [
  163. ('email', '=', email_normalize(message.email_from)),
  164. ('mail_group_id', '=', message.mail_group_id.id)
  165. ] for message in self
  166. ])
  167. )
  168. existing_moderation.status = status
  169. # Add the value in a set to create only 1 moderation rule per (email_normalized, group)
  170. moderation_to_create = {
  171. (email_normalize(message.email_from), message.mail_group_id.id)
  172. for message in self
  173. if email_normalize(message.email_from) not in existing_moderation.mapped('email')
  174. }
  175. self.env['mail.group.moderation'].create([
  176. {
  177. 'email': email,
  178. 'mail_group_id': mail_group_id,
  179. 'status': status,
  180. } for email, mail_group_id in moderation_to_create])
  181. def _assert_moderable(self):
  182. """Raise an error if one of the current message can not be moderated.
  183. A <mail.group.message> can only be moderated
  184. if it's moderation status is "pending_moderation".
  185. """
  186. non_moderable_messages = self.filtered_domain([
  187. ('moderation_status', '!=', 'pending_moderation'),
  188. ])
  189. if non_moderable_messages:
  190. if len(self) == 1:
  191. raise UserError(_('This message can not be moderated'))
  192. raise UserError(_(
  193. 'Those messages can not be moderated: %s.',
  194. ', '.join(non_moderable_messages.mapped('subject')),
  195. ))
  196. def _moderate_send_reject_email(self, subject, comment):
  197. for message in self:
  198. if not message.email_from:
  199. continue
  200. body_html = append_content_to_html('<div>%s</div>' % ustr(comment), message.body, plaintext=False)
  201. body_html = self.env['mail.render.mixin']._replace_local_links(body_html)
  202. self.env['mail.mail'].sudo().create({
  203. 'author_id': self.env.user.partner_id.id,
  204. 'auto_delete': True,
  205. 'body_html': body_html,
  206. 'email_from': self.env.user.email_formatted or self.env.company.catchall_formatted,
  207. 'email_to': message.email_from,
  208. 'references': message.mail_message_id.message_id,
  209. 'subject': subject,
  210. 'state': 'outgoing',
  211. })