From 299ebb2cdf63de2e3967da0d2f3e7effd239812b Mon Sep 17 00:00:00 2001
From: Mathieu Duckaerts-Antoine <dam@odoo.com>
Date: Thu, 24 May 2018 15:27:28 +0200
Subject: [PATCH] [IMP] mail: add moderation on channels

Purpose of this commit is to allow moderation on incoming messages in
discussion channels. On some channels on which moderation is required
messages should be in a pending moderation stage. Moderators can accept
or refuse messages as well as always allow or ban messages coming from
a given set of emails.

Channels now have an option to be moderated. Moderators can be added on
channels. They have access to a specific UI in Discuss to see and take
action on messages waiting for moderation.

Concerning mail.thread message that are pending moderation are not notified.
It means nobody receives a notification about them. Moderation process calls
the notification once the message is validated.

Various features included in this commit :

 * a model is added to store the decision about emails, allow or ban;
 * access rights are updated so that only moderators can modify moderation
   fields on message;
 * specific bus notifications are send to moderated people as well as to
   moderators on incoming emails as well as when a decision is taken;
 * options are added on channels to send explanations to moderated emails;
 * options are added on channels to write and send guidelines explaining
   why and how moderation is performed;
 * a reminder is send daily to moderators with remaining messages to moderate;
 * discuss UI is adapted and a new channel is added below Inbox and Starred
   giving access to moderation tools;
 * chanenl UI is adapted allowing to moderate directly inside channels;

This commit is linked to task ID 29521. Closes #21921.
---
 addons/mail/__manifest__.py                   |   1 +
 addons/mail/controllers/main.py               |   3 +
 addons/mail/data/ir_cron_data.xml             |  15 +-
 addons/mail/data/mail_data.xml                |  32 +
 addons/mail/models/__init__.py                |   1 +
 addons/mail/models/mail_channel.py            | 220 ++++-
 addons/mail/models/mail_message.py            | 295 ++++++-
 addons/mail/models/mail_thread.py             |  24 +-
 addons/mail/models/res_company.py             |  23 +
 addons/mail/models/res_users.py               |  35 +-
 addons/mail/security/ir.model.access.csv      |   1 +
 addons/mail/security/mail_security.xml        |   5 +
 addons/mail/static/src/js/discuss.js          | 359 +++++++-
 .../static/src/js/services/chat_manager.js    | 118 ++-
 addons/mail/static/src/js/thread.js           |  29 +-
 addons/mail/static/src/xml/discuss.xml        |  15 +
 addons/mail/static/src/xml/thread.xml         |  36 +-
 .../static/tests/discuss_moderation_tests.js  | 822 ++++++++++++++++++
 addons/mail/static/tests/discuss_tests.js     |  22 +-
 addons/mail/views/mail_channel_views.xml      |  23 +-
 addons/mail/views/mail_message_views.xml      |   2 +
 addons/mail/views/mail_moderation_views.xml   |  52 ++
 addons/mail/views/mail_templates.xml          |   1 +
 addons/test_mail/tests/__init__.py            |   3 +-
 addons/test_mail/tests/common.py              |  58 +-
 addons/test_mail/tests/test_mail_channel.py   | 104 ++-
 addons/test_mail/tests/test_mail_message.py   | 105 ++-
 addons/test_mail/tests/test_res_users.py      |  34 +
 28 files changed, 2347 insertions(+), 91 deletions(-)
 create mode 100644 addons/mail/models/res_company.py
 create mode 100644 addons/mail/static/tests/discuss_moderation_tests.js
 create mode 100644 addons/mail/views/mail_moderation_views.xml
 create mode 100644 addons/test_mail/tests/test_res_users.py

diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py
index 5bdfbc3ae618..1638baae47ef 100644
--- a/addons/mail/__manifest__.py
+++ b/addons/mail/__manifest__.py
@@ -32,6 +32,7 @@
         'views/mail_templates.xml',
         'wizard/email_template_preview_view.xml',
         'views/mail_template_views.xml',
+        'views/mail_moderation_views.xml',
         'views/ir_actions_views.xml',
         'views/ir_model_views.xml',
         'views/res_partner_views.xml',
diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py
index 9a57808f057f..f4b6f6b8d5d7 100644
--- a/addons/mail/controllers/main.py
+++ b/addons/mail/controllers/main.py
@@ -228,5 +228,8 @@ class MailController(http.Controller):
             'mention_partner_suggestions': request.env['res.partner'].get_static_mention_suggestions(),
             'shortcodes': request.env['mail.shortcode'].sudo().search_read([], ['source', 'substitution', 'description']),
             'menu_id': request.env['ir.model.data'].xmlid_to_res_id('mail.mail_channel_menu_root_chat'),
+            'is_moderator': request.env.user.is_moderator,
+            'moderation_counter': request.env.user.moderation_counter,
+            'moderation_channel_ids': request.env.user.moderation_channel_ids.ids,
         }
         return values
diff --git a/addons/mail/data/ir_cron_data.xml b/addons/mail/data/ir_cron_data.xml
index 8b742a7c02d7..d1012c9cae64 100644
--- a/addons/mail/data/ir_cron_data.xml
+++ b/addons/mail/data/ir_cron_data.xml
@@ -26,5 +26,18 @@
             <field eval="False" name="doall" />
             <field name="priority">1000</field>
         </record>
+		
+        <record id="ir_cron_mail_notify_channel_moderators" model="ir.cron">
+            <field name="name">Mail: Notify channel moderators</field>
+            <field name="model_id" ref="model_mail_message"/>
+            <field name="state">code</field>
+            <field name="code">model._notify_moderators</field>
+            <field name="user_id" ref="base.user_root" />
+            <field name="interval_number">1</field>
+            <field name="interval_type">days</field>
+            <field name="numbercall">-1</field>
+            <field eval="False" name="doall" />
+            <field name="priority">1000</field>
+        </record>
     </data>
-</odoo>
\ No newline at end of file
+</odoo>
diff --git a/addons/mail/data/mail_data.xml b/addons/mail/data/mail_data.xml
index 11f0117066f1..fbbc41f5202f 100644
--- a/addons/mail/data/mail_data.xml
+++ b/addons/mail/data/mail_data.xml
@@ -226,5 +226,37 @@
 </td></tr>
 </table>
         </template>
+
+        <!-- Channel and moderation related data -->
+        <template id="mail_channel_notify_moderation">
+<div>
+    <p>Hello <t t-esc='record.name'/></p>
+    <p>You have messages to moderate, please go for the proceedings.</p><br/><br/>
+    <div style="text-align: center;">
+        <a href="/web#action=mail.mail_channel_action_client_chat&amp;active_id=channel_moderation" style="background-color: #1abc9c; padding: 20px; text-decoration: none; color: #fff; border-radius: 5px; font-size: 16px;" class="o_default_snippet_text">Moderate Messages</a>
+        <br/><br/><br/>
+    </div>
+    <p>Thank you!</p>
+</div>
+        </template>
+
+        <record id="mail_template_channel_send_guidelines" model="mail.template">
+            <field name="name">Partner: Send channel guidelines</field>
+            <field name="model_id" ref="base.model_res_partner"/>
+            <field name="email_from">${object.company_id.catchall or object.company_id.email|safe}</field>
+            <field name="partner_to">${object.id}</field>
+            <field name="subject">Guidelines of channel ${ctx['channel'].name}</field>
+            <field name="body_html" type="xml">
+<div>
+<p>Hello ${object.name or ''},</p>
+<p>Please find below the guidelines of the ${ctx['channel'].name} channel.</p>
+<p>
+${ctx['channel'].moderation_guidelines_msg}
+</p>
+<p></p>
+</div>
+</field>
+            <field name="user_signature" eval="False"/>
+        </record>
     </data>
 </odoo>
diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py
index daf70f23ca42..a378f9e719d8 100644
--- a/addons/mail/models/__init__.py
+++ b/addons/mail/models/__init__.py
@@ -14,6 +14,7 @@ from . import mail_template
 from . import mail_shortcode
 from . import res_partner
 from . import res_users
+from . import res_company
 from . import res_config_settings
 from . import update
 from . import ir_actions
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index 9da0862376f6..edb0eb0e53e2 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -1,17 +1,22 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
-import base64
-from email.utils import formataddr
 
+import base64
+import logging
 import re
+
+from email.utils import formataddr
 from uuid import uuid4
 
 from odoo import _, api, fields, models, modules, tools
-from odoo.exceptions import UserError
+from odoo.exceptions import UserError, ValidationError
 from odoo.osv import expression
-from odoo.tools import ormcache
+from odoo.tools import ormcache, pycompat
 from odoo.tools.safe_eval import safe_eval
 
+MODERATION_FIELDS = ['moderation', 'moderator_ids', 'moderation_ids', 'moderation_notify', 'moderation_notify_msg', 'moderation_guidelines', 'moderation_guidelines_msg']
+_logger = logging.getLogger(__name__)
+
 
 class ChannelPartner(models.Model):
     _name = 'mail.channel.partner'
@@ -28,6 +33,22 @@ class ChannelPartner(models.Model):
     is_pinned = fields.Boolean("Is pinned on the interface", default=True)
 
 
+class Moderation(models.Model):
+    _name = 'mail.moderation'
+    _description = 'Channel black/white list'
+
+    email = fields.Char(string="Email", index=True, required=True)
+    status = fields.Selection([
+        ('allow', 'Always Allow'),
+        ('ban', 'Permanent Ban')],
+        string="Status", required=True)
+    channel_id = fields.Many2one('mail.channel', string="Channel", index=True, required=True)
+
+    _sql_constraints = [
+        ('channel_email_uniq', 'unique (email,channel_id)', 'The email address must be unique per channel !')
+    ]
+
+
 class Channel(models.Model):
     """ A mail.channel is a discussion group that may behave like a listener
     on documents. """
@@ -90,12 +111,61 @@ class Channel(models.Model):
              "Use this field anywhere a small image is required.")
     is_subscribed = fields.Boolean(
         'Is Subscribed', compute='_compute_is_subscribed')
+    # moderation
+    moderation = fields.Boolean(string='Moderate this channel')
+    moderator_ids = fields.Many2many('res.users', 'mail_channel_moderator_rel', string='Moderators')
+    is_moderator = fields.Boolean(help="Current user is a moderator of the channel", string='Moderator', compute="_compute_is_moderator")
+    moderation_ids = fields.One2many(
+        'mail.moderation', 'channel_id', string='Moderated Emails',
+        groups="base.group_user")
+    moderation_count = fields.Integer(
+        string='Moderated emails count', compute='_compute_moderation_count',
+        groups="base.group_user")
+    moderation_notify = fields.Boolean(string="Automatic notification", help="People receive an automatic notification about their message being waiting for moderation.")
+    moderation_notify_msg = fields.Text(string="Notification message")
+    moderation_guidelines = fields.Boolean(string="Send guidelines to new subscribers", help="Newcomers on this moderated channel will automatically receive the guidelines.")
+    moderation_guidelines_msg = fields.Text(string="Guidelines")
 
     @api.one
     @api.depends('channel_partner_ids')
     def _compute_is_subscribed(self):
         self.is_subscribed = self.env.user.partner_id in self.channel_partner_ids
 
+    @api.multi
+    @api.depends('moderator_ids')
+    def _compute_is_moderator(self):
+        for channel in self:
+            channel.is_moderator = self.env.user in channel.moderator_ids
+
+    @api.multi
+    @api.depends('moderation_ids')
+    def _compute_moderation_count(self):
+        read_group_res = self.env['mail.moderation'].read_group([('channel_id', 'in', self.ids)], ['channel_id'], 'channel_id')
+        data = dict((res['channel_id'][0], res['channel_id_count']) for res in read_group_res)
+        for channel in self:
+            channel.moderation_count = data.get(channel.id, 0)
+
+    @api.constrains('moderator_ids')
+    def _check_moderator_email(self):
+        if any(not moderator.email for channel in self for moderator in channel.moderator_ids):
+            raise ValidationError("Moderators must have an email address.")
+
+    @api.constrains('moderator_ids', 'channel_partner_ids', 'channel_last_seen_partner_ids')
+    def _check_moderator_is_member(self):
+        for channel in self:
+            if not (channel.mapped('moderator_ids.partner_id') <= channel.channel_partner_ids):
+                raise ValidationError("Moderators should be members of the channel they moderate.")
+
+    @api.constrains('moderation', 'email_send')
+    def _check_moderation_parameters(self):
+        if any(not channel.email_send and channel.moderation for channel in self):
+            raise ValidationError('Only mailing lists can be moderated.')
+
+    @api.constrains('moderator_ids')
+    def _check_moderator_existence(self):
+        if any(not channel.moderator_ids for channel in self if channel.moderation):
+            raise ValidationError('Moderated channels must have moderators.')
+
     @api.multi
     def _compute_is_member(self):
         memberships = self.env['mail.channel.partner'].sudo().search([
@@ -113,6 +183,26 @@ class Channel(models.Model):
         else:
             self.alias_contact = 'followers'
 
+    @api.onchange('moderator_ids')
+    def _onchange_moderator_ids(self):
+        missing_partners = self.mapped('moderator_ids.partner_id') - self.channel_partner_ids
+        if missing_partners:
+            self.channel_partner_ids |= missing_partners
+
+    @api.onchange('email_send')
+    def _onchange_email_send(self):
+        if not self.email_send:
+            self.moderation = False
+
+    @api.onchange('moderation')
+    def _onchange_moderation(self):
+        if not self.moderation:
+            self.moderation_notify = False
+            self.moderation_guidelines = False
+            self.moderator_ids = False
+        else:
+            self.moderator_ids |= self.env.user
+
     @api.model
     def create(self, vals):
         # ensure image at quick create
@@ -154,10 +244,25 @@ class Channel(models.Model):
 
     @api.multi
     def write(self, vals):
+        # First checks if user tries to modify moderation fields and has not the right to do it.
+        if any(key for key in MODERATION_FIELDS if vals.get(key)) and any(self.env.user not in channel.moderator_ids for channel in self if channel.moderation):
+            if not self.env.user.has_group('base.group_system'):
+                raise UserError("You do not possess the rights to modify fields related to moderation on one of the channels you are modifying.")
+
         tools.image_resize_images(vals)
         result = super(Channel, self).write(vals)
+
         if vals.get('group_ids'):
             self._subscribe_users()
+
+        # avoid keeping messages to moderate and accept them
+        if vals.get('moderation') is False:
+            self.env['mail.message'].search([
+                ('moderation_status', '=', 'pending_moderation'),
+                ('model', '=', 'mail.channel'),
+                ('res_id', 'in', self.ids)
+            ])._moderate_accept()
+
         return result
 
     def get_alias_model_name(self, vals):
@@ -240,12 +345,54 @@ class Channel(models.Model):
             }
         return super(Channel, self)._notify_email_recipients(message, recipient_ids)
 
+    def _extract_moderation_values(self, message_type, **kwargs):
+        """ This method is used to compute moderation status before the creation
+        of a message.  For this operation the message's author email address is required.
+        This address is returned with status for other computations. """
+        moderation_status = 'accepted'
+        email = ''
+        if self.moderation and message_type in ['email', 'comment']:
+            author_id = kwargs.get('author_id')
+            if author_id and isinstance(author_id, pycompat.integer_types):
+                email = self.env['res.partner'].browse([author_id]).email
+            elif author_id:
+                email = author_id.email
+            elif kwargs.get('email_from'):
+                email = tools.email_split(kwargs['email_from'])[0]
+            else:
+                email = self.env.user.email
+            if email in self.mapped('moderator_ids.email'):
+                return moderation_status, email
+            status = self.env['mail.moderation'].sudo().search([('email', '=', email), ('channel_id', 'in', self.ids)]).mapped('status')
+            if status and status[0] == 'allow':
+                moderation_status = 'accepted'
+            elif status and status[0] == 'ban':
+                moderation_status = 'rejected'
+            else:
+                moderation_status = 'pending_moderation'
+        return moderation_status, email
+
     @api.multi
-    @api.returns('self', lambda value: value.id)
-    def message_post(self, **kwargs):
-        # auto pin 'direct_message' channel partner
+    @api.returns('mail.message', lambda value: value.id)
+    def message_post(self, message_type='notification', **kwargs):
+        moderation_status, email = self._extract_moderation_values(message_type, **kwargs)
+        if moderation_status == 'rejected':
+            return self.env['mail.message']
+
         self.filtered(lambda channel: channel.channel_type == 'chat').mapped('channel_last_seen_partner_ids').write({'is_pinned': True})
-        message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(**kwargs)
+
+        message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(message_type=message_type, moderation_status=moderation_status, **kwargs)
+
+        # Notifies the message author when his message is pending moderation if required on channel.
+        # The fields "email_from" and "reply_to" are filled in automatically by method create in model mail.message.
+        if self.moderation_notify and self.moderation_notify_msg and message_type == 'email' and moderation_status == 'pending_moderation':
+            self.env['mail.mail'].create({
+                'body_html': self.moderation_notify_msg,
+                'subject': 'Re: %s' % (kwargs.get('subject', '')),
+                'email_to': email,
+                'auto_delete': True,
+                'state': 'outgoing'
+            })
         return message
 
     def _alias_check_contact(self, message, message_dict, alias):
@@ -264,6 +411,58 @@ class Channel(models.Model):
         if not self._cr.fetchone():
             self._cr.execute('CREATE INDEX mail_channel_partner_seen_message_id_idx ON mail_channel_partner (channel_id,partner_id,seen_message_id)')
 
+    # --------------------------------------------------
+    # Moderation
+    # --------------------------------------------------
+
+    @api.multi
+    def send_guidelines(self):
+        """ Send guidelines to all channel members. """
+        if self.env.user in self.moderator_ids or self.env.user.has_group('base.group_system'):
+            success = self._send_guidelines(self.channel_partner_ids)
+            if not success:
+                raise UserError('Template "mail.mail_template_channel_send_guidelines" was not found. No email has been sent. Please contact an administrator to fix this issue.')
+        else:
+            raise UserError("Only an administrator or a moderator can send guidelines to channel members!")
+
+    @api.multi
+    def _send_guidelines(self, partners):
+        """ Send guidelines of a given channel. Returns False if template used for guidelines
+        not found. Caller may have to handle this return value. """
+        self.ensure_one()
+        template = self.env.ref('mail.mail_template_channel_send_guidelines', raise_if_not_found=False)
+        if not template:
+            _logger.warning('Template "mail.mail_template_channel_send_guidelines" was not found.')
+            return False
+        banned_emails = self.env['mail.moderation'].sudo().search([
+            ('status', '=', 'ban'),
+            ('channel_id', 'in', self.ids)
+        ]).mapped('email')
+        for partner in partners.filtered(lambda p: p.email and not (p.email in banned_emails)):
+            # the sudo is needed because because send_mail will create a message (a mail). As the template is
+            # linked to res.partner model the current user could not have the rights to modify this model. It
+            # means it could prevent users to create the mail.message necessary for the mail.mail.
+            template.with_context(lang=partner.lang, channel=self).sudo().send_mail(partner.id)
+        return True
+
+    @api.multi
+    def _update_moderation_email(self, emails, status):
+        """ This method adds emails into either white or black of the channel list of emails 
+            according to status. If an email in emails is already moderated, the method updates the email status.
+            :param emails: list of email addresses to put in white or black list of channel.
+            :param status: value is 'allow' or 'ban'. Emails are put in white list if 'allow', in black list if 'ban'.
+        """
+        self.ensure_one()
+        splitted_emails = [tools.email_split(email)[0] for email in emails if tools.email_split(email)]
+        moderated = self.env['mail.moderation'].sudo().search([
+            ('email', 'in', splitted_emails),
+            ('channel_id', 'in', self.ids)
+        ])
+        cmds = [(1, record.id, {'status': status}) for record in moderated]
+        not_moderated = [email for email in splitted_emails if email not in moderated.mapped('email')]
+        cmds += [(0, 0, {'email': email, 'status': status}) for email in not_moderated]
+        return self.write({'moderation_ids': cmds})
+
     #------------------------------------------------------
     # Instant Messaging API
     #------------------------------------------------------
@@ -347,6 +546,8 @@ class Channel(models.Model):
                 'channel_type': channel.channel_type,
                 'public': channel.public,
                 'mass_mailing': channel.email_send,
+                'moderation': channel.moderation,
+                'is_moderator': self.env.uid in channel.moderator_ids.ids,
                 'group_based_subscription': bool(channel.group_ids),
             }
             if extra_info:
@@ -572,6 +773,9 @@ class Channel(models.Model):
             self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
         self.action_follow()
 
+        if self.moderation_guidelines:
+            self._send_guidelines(self.env.user.partner_id)
+
         channel_info = self.channel_info()[0]
         self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info)
         return channel_info
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index b42de71abcfd..d98c98fb1d9b 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -107,6 +107,13 @@ class Message(models.Model):
     message_id = fields.Char('Message-Id', help='Message unique identifier', index=True, readonly=1, copy=False)
     reply_to = fields.Char('Reply-To', help='Reply email address. Setting the reply_to bypasses the automatic thread creation.')
     mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server')
+    # moderation
+    moderation_status = fields.Selection([
+        ('pending_moderation', 'Pending Moderation'),
+        ('accepted', 'Accepted'),
+        ('rejected', 'Rejected')], string="Moderation Status", index=True)
+    moderator_id = fields.Many2one('res.users', string="Moderated By", index=True)
+    need_moderation = fields.Boolean('Need moderation', compute='_compute_need_moderation', search='_search_need_moderation')
 
     @api.multi
     def _get_needaction(self):
@@ -138,6 +145,23 @@ class Message(models.Model):
             return [('starred_partner_ids', 'in', [self.env.user.partner_id.id])]
         return [('starred_partner_ids', 'not in', [self.env.user.partner_id.id])]
 
+    @api.multi
+    def _compute_need_moderation(self):
+        for message in self:
+            self.need_moderation = False
+
+    @api.model
+    def _search_need_moderation(self, operator, operand):
+        if operator == '=' and operand:
+            return ['&', '&',
+                    ('moderation_status', '=', 'pending_moderation'),
+                    ('model', '=', 'mail.channel'),
+                    ('res_id', 'in', self.env.user.moderation_channel_ids.ids)]
+        return ['|', '|',
+                ('moderation_status', '!=', 'pending_moderation'),
+                ('model', '!=', 'mail.channel'),
+                ('res_id', 'not in', self.env.user.moderation_channel_ids.ids)]
+
     #------------------------------------------------------
     # Notification API
     #------------------------------------------------------
@@ -395,6 +419,7 @@ class Message(models.Model):
                     'is_note': True # only if the message is a note (subtype == note)
                     'is_discussion': False # only if the message is a discussion (subtype == discussion)
                     'is_notification': False # only if the message is a note but is a notification aka not linked to a document like assignation
+                    'moderation_status': 'pending_moderation'
                 }
         """
         message_values = self.read([
@@ -403,6 +428,7 @@ class Message(models.Model):
             'model', 'res_id', 'record_name',  # document related
             'channel_ids', 'partner_ids',  # recipients
             'starred_partner_ids',  # list of partner ids for whom the message is starred
+            'moderation_status',
         ])
         message_tree = dict((m.id, m) for m in self.sudo())
         self._message_read_dict_postprocess(message_values, message_tree)
@@ -474,8 +500,7 @@ class Message(models.Model):
         - if author_id == pid, uid is the author, OR
         - uid belongs to a notified channel, OR
         - uid is in the specified recipients, OR
-        - uid has a notification on the message, OR
-        - uid have read access to the related document is model, res_id
+        - uid has a notification on the message
         - otherwise: remove the id
         """
         # Rules do not apply to administrator
@@ -517,6 +542,7 @@ class Message(models.Model):
             ON channel.id = channel_rel.mail_channel_id
             LEFT JOIN "mail_channel_partner" channel_partner
             ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
+
             WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=pid, ids=ids))
         for id, rmod, rid, author_id, partner_id, channel_id in self._cr.fetchall():
             if author_id == pid:
@@ -558,10 +584,12 @@ class Message(models.Model):
             - write: if
                 - author_id == pid, uid is the author, OR
                 - uid is in the recipients (partner_ids) OR
-                - uid has write or create access on the related document if model, res_id
+                - uid is moderator of the channel and moderation_status is pending_moderation OR
+                - uid has write or create access on the related document if model, res_id and moderation_status is not pending_moderation
                 - otherwise: raise
             - unlink: if
-                - uid has write or create access on the related document if model, res_id
+                - uid is moderator of the channel and moderation_status is pending_moderation OR
+                - uid has write or create access on the related document if model, res_id and moderation_status is not pending_moderation
                 - otherwise: raise
 
         Specific case: non employee users see only messages with subtype (aka do
@@ -595,9 +623,9 @@ class Message(models.Model):
         # Read mail_message.ids to have their values
         message_values = dict((res_id, {}) for res_id in self.ids)
 
-        if operation in ['read', 'write']:
+        if operation == 'read':
             self._cr.execute("""
-                SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id,
+                SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, m.moderation_status,
                                 COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
                                 channel_partner.channel_id as channel_id
                 FROM "%s" m
@@ -612,24 +640,84 @@ class Message(models.Model):
                 LEFT JOIN "mail_channel_partner" channel_partner
                 ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
                 WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=self.env.user.partner_id.id, ids=self.ids))
-            for mid, rmod, rid, author_id, parent_id, partner_id, channel_id in self._cr.fetchall():
+            for mid, rmod, rid, author_id, parent_id, partner_id, channel_id, moderation_status in self._cr.fetchall():
                 message_values[mid] = {
                     'model': rmod,
                     'res_id': rid,
                     'author_id': author_id,
                     'parent_id': parent_id,
+                    'moderation_status': moderation_status,
+                    'moderator_id': False,
                     'notified': any((message_values[mid].get('notified'), partner_id, channel_id))
                 }
-        else:
-            self._cr.execute("""SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)""" % self._table, (self.ids,))
-            for mid, rmod, rid, author_id, parent_id in self._cr.fetchall():
-                message_values[mid] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id}
+        elif operation == 'write':
+            self._cr.execute("""
+                SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, m.moderation_status,
+                                COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
+                                channel_partner.channel_id as channel_id, channel_moderator_rel.res_users_id as moderator_id
+                FROM "%s" m
+                LEFT JOIN "mail_message_res_partner_rel" partner_rel
+                ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s
+                LEFT JOIN "mail_message_res_partner_needaction_rel" needaction_rel
+                ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_partner_id = %%(pid)s
+                LEFT JOIN "mail_message_mail_channel_rel" channel_rel
+                ON channel_rel.mail_message_id = m.id
+                LEFT JOIN "mail_channel" channel
+                ON channel.id = channel_rel.mail_channel_id
+                LEFT JOIN "mail_channel_partner" channel_partner
+                ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
+                LEFT JOIN "mail_channel" moderated_channel
+                ON m.moderation_status = 'pending_moderation' AND m.res_id = moderated_channel.id
+                LEFT JOIN "mail_channel_moderator_rel" channel_moderator_rel
+                ON channel_moderator_rel.mail_channel_id = moderated_channel.id AND channel_moderator_rel.res_users_id = %%(uid)s
+                WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=self.env.user.partner_id.id, uid=self.env.user.id, ids=self.ids))
+            for mid, rmod, rid, author_id, parent_id, moderation_status, partner_id, channel_id, moderator_id in self._cr.fetchall():
+                message_values[mid] = {
+                    'model': rmod,
+                    'res_id': rid,
+                    'author_id': author_id,
+                    'parent_id': parent_id,
+                    'moderation_status': moderation_status,
+                    'moderator_id': moderator_id,
+                    'notified': any((message_values[mid].get('notified'), partner_id, channel_id))
+                }
+        elif operation == 'create':
+            self._cr.execute("""SELECT DISTINCT id, model, res_id, author_id, parent_id, moderation_status FROM "%s" WHERE id = ANY (%%s)""" % self._table, (self.ids,))
+            for mid, rmod, rid, author_id, parent_id, moderation_status in self._cr.fetchall():
+                message_values[mid] = {
+                    'model': rmod,
+                    'res_id': rid,
+                    'author_id': author_id,
+                    'parent_id': parent_id,
+                    'moderation_status': moderation_status,
+                    'moderator_id': False
+                }
+        else:  # unlink
+            self._cr.execute("""SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, m.moderation_status, channel_moderator_rel.res_users_id as moderator_id
+                FROM "%s" m
+                LEFT JOIN "mail_channel" moderated_channel
+                ON m.moderation_status = 'pending_moderation' AND m.res_id = moderated_channel.id
+                LEFT JOIN "mail_channel_moderator_rel" channel_moderator_rel
+                ON channel_moderator_rel.mail_channel_id = moderated_channel.id AND channel_moderator_rel.res_users_id = (%%s)
+                WHERE m.id = ANY (%%s)""" % self._table, (self.env.user.id, self.ids,))
+            for mid, rmod, rid, author_id, parent_id, moderation_status, moderator_id in self._cr.fetchall():
+                message_values[mid] = {
+                    'model': rmod,
+                    'res_id': rid,
+                    'author_id': author_id,
+                    'parent_id': parent_id,
+                    'moderation_status': moderation_status,
+                    'moderator_id': moderator_id
+                }
 
         # Author condition (READ, WRITE, CREATE (private))
         author_ids = []
-        if operation == 'read' or operation == 'write':
+        if operation == 'read':
             author_ids = [mid for mid, message in message_values.items()
                           if message.get('author_id') and message.get('author_id') == self.env.user.partner_id.id]
+        elif operation == 'write':
+            author_ids = [mid for mid, message in message_values.items()
+                          if message.get('moderation_status') != 'pending_moderation' and message.get('author_id') == self.env.user.partner_id.id]
         elif operation == 'create':
             author_ids = [mid for mid, message in message_values.items()
                           if not message.get('model') and not message.get('res_id')]
@@ -654,8 +742,13 @@ class Message(models.Model):
             notified_ids += [mid for mid, message in message_values.items()
                              if message.get('parent_id') in not_parent_ids]
 
+        # Moderator condition: allow to WRITE, UNLINK if moderator of a pending message
+        moderator_ids = []
+        if operation in ['write', 'unlink']:
+            moderator_ids = [mid for mid, message in message_values.items() if message.get('moderator_id')]
+
         # Recipients condition, for read and write (partner_ids) and create (message_follower_ids)
-        other_ids = set(self.ids).difference(set(author_ids), set(notified_ids))
+        other_ids = set(self.ids).difference(set(author_ids), set(notified_ids), set(moderator_ids))
         model_record_ids = _generate_model_record_ids(message_values, other_ids)
         if operation in ['read', 'write']:
             notified_ids = [mid for mid, message in message_values.items() if message.get('notified')]
@@ -681,8 +774,13 @@ class Message(models.Model):
                 DocumentModel.check_mail_message_access(mids.ids, operation)  # ?? mids ?
             else:
                 self.env['mail.thread'].check_mail_message_access(mids.ids, operation, model_name=model)
-            document_related_ids += [mid for mid, message in message_values.items()
-                                     if message.get('model') == model and message.get('res_id') in mids.ids]
+            if operation in ['write', 'unlink']:
+                document_related_ids += [mid for mid, message in message_values.items()
+                                         if message.get('model') == model and message.get('res_id') in mids.ids and
+                                         message.get('moderation_status') != 'pending_moderation']
+            else:
+                document_related_ids += [mid for mid, message in message_values.items()
+                                         if message.get('model') == model and message.get('res_id') in mids.ids]
 
         # Calculate remaining ids: if not void, raise an error
         other_ids = other_ids.difference(set(document_related_ids))
@@ -841,7 +939,6 @@ class Message(models.Model):
         # remove author from notified partners
         if not self._context.get('mail_notify_author', False) and self_sudo.author_id:
             partners_sudo = partners_sudo - self_sudo.author_id
-
         # update message, with maybe custom values
         message_values = {}
         if channels_sudo:
@@ -858,6 +955,7 @@ class Message(models.Model):
         # have no access to partner model. Maybe propagating a real uid could be necessary.
         email_channels = channels_sudo.filtered(lambda channel: channel.email_send)
         notif_partners = partners_sudo.filtered(lambda partner: 'inbox' in partner.mapped('user_ids.notification_type'))
+
         if email_channels or partners_sudo - notif_partners:
             partners_sudo.search([
                 '|',
@@ -867,7 +965,170 @@ class Message(models.Model):
             ])._notify(self, layout=layout, force_send=force_send, send_after_commit=send_after_commit, values=values)
 
         notif_partners._notify_by_chat(self)
-
         channels_sudo._notify(self)
 
         return True
+
+    # --------------------------------------------------
+    # Moderation
+    # --------------------------------------------------
+
+    @api.multi
+    def moderate(self, decision, **kwargs):
+        """ Moderate messages. A check is done on moderation status of the
+        current user to ensure we only moderate valid messages. """
+        moderated_channels = self.env.user.moderation_channel_ids
+        to_moderate = [message.id for message in self
+                       if message.model == 'mail.channel' and
+                       message.res_id in moderated_channels.ids and
+                       message.moderation_status == 'pending_moderation']
+        if to_moderate:
+            self.browse(to_moderate)._moderate(decision, **kwargs)
+
+    @api.multi
+    def _moderate(self, decision, **kwargs):
+        """ :param decision
+                 * accept       - moderate message and broadcast that message to followers of relevant channels.
+                 * reject       - message will be deleted from the database without broadcast
+                                  an email sent to the author with an explanation that the moderators can edit.
+                 * discard      - message will be deleted from the database without broadcast.
+                 * allow        - add email address to white list people of specific channel,
+                                  so that next time if a message come from same email address on same channel,
+                                  it will be automatically broadcasted to relevant channels without any approval from moderator.
+                 * ban          - add email address to black list of emails for the specific channel.
+                                  From next time, a person sending a message using that email address will not need moderation.
+                                  message_post will not create messages with the corresponding expeditor.
+        """
+        if decision == 'accept':
+            self._moderate_accept()
+        elif decision == 'reject':
+            self._moderate_send_reject_email(kwargs.get('title'), kwargs.get('comment'))
+            self._moderate_discard()
+        elif decision == 'discard':
+            self._moderate_discard()
+        elif decision == 'allow':
+            channels = self.env['mail.channel'].browse(self.mapped('res_id'))
+            for channel in channels:
+                channel._update_moderation_email(
+                    list({message.email_from for message in self if message.res_id == channel.id}),
+                    'allow'
+                )
+            self._search_from_same_authors()._moderate_accept()
+        elif decision == 'ban':
+            channels = self.env['mail.channel'].browse(self.mapped('res_id'))
+            for channel in channels:
+                channel._update_moderation_email(
+                    list({message.email_from for message in self if message.res_id == channel.id}),
+                    'ban'
+                )
+            self._search_from_same_authors()._moderate_discard()
+
+    def _moderate_accept(self):
+        self.write({
+            'moderation_status': 'accepted',
+            'moderator_id': self.env.uid
+        })
+        # proceed with notification process to send notification emails and Inbox messages
+        for message in self:
+            message._notify()
+
+    @api.multi
+    def _moderate_send_reject_email(self, subject, comment):
+        for msg in self:
+            if not msg.email_from:
+                continue
+            if self.env.user.partner_id.email:
+                email_from = formataddr((self.env.user.partner_id.name, self.env.user.partner_id.email))
+            else:
+                email_from = self.env.user.company_id.catchall
+
+            body_html = tools.append_content_to_html('<div>%s</div>' % tools.ustr(comment), msg.body)
+            vals = {
+                'subject': subject,
+                'body_html': body_html,
+                'email_from': email_from,
+                'email_to': msg.email_from,
+                'auto_delete': True,
+                'state': 'outgoing'
+            }
+            self.env['mail.mail'].sudo().create(vals)
+
+    @api.multi
+    def _search_from_same_authors(self):
+        """ Returns all pending moderation messages that have same email_from and
+        same res_id as given recordset. """
+        messages = self.env['mail.message'].sudo()
+        for message in self:
+            messages |= messages.search([
+                ('moderation_status', '=', 'pending_moderation'),
+                ('email_from', '=', message.email_from),
+                ('model', '=', 'mail.channel'),
+                ('res_id', '=', message.res_id)
+            ])
+        return messages
+
+    @api.multi
+    def _moderate_discard(self):
+        """ Notify deletion of messages to their moderators and authors and then delete them.
+        """
+        channel_ids = self.mapped('res_id')
+        moderators = self.env['mail.channel'].browse(channel_ids).mapped('moderator_ids')
+        authors = self.mapped('author_id')
+        partner_to_pid = {}
+        for moderator in moderators:
+            partner_to_pid.setdefault(moderator.partner_id.id, set())
+            partner_to_pid[moderator.partner_id.id] |= set([message.id for message in self if message.res_id in moderator.moderation_channel_ids.ids])
+        for author in authors:
+            partner_to_pid.setdefault(author.id, set())
+            partner_to_pid[author.id] |= set([message.id for message in self if message.author_id == author])
+
+        notifications = []
+        for partner_id, message_ids in partner_to_pid.items():
+            notifications.append([
+                (self._cr.dbname, 'res.partner', partner_id),
+                {'type': 'deletion', 'message_ids': list(message_ids)}
+            ])
+        self.env['bus.bus'].sendmany(notifications)
+        self.unlink()
+
+    def _notify_pending_by_chat(self):
+        """ Generate the bus notifications for the given message and send them
+        to the appropriate moderators and the author (if the author has not been
+        elected moderator meanwhile). The author notification can be considered
+        as a feedback to the author.
+        """
+        self.ensure_one()
+        message = self.message_format()[0]
+        partners = self.env['mail.channel'].browse(self.res_id).mapped('moderator_ids.partner_id')
+        notifications = []
+        for partner in partners:
+            notifications.append([
+                (self._cr.dbname, 'res.partner', partner.id),
+                {'type': 'moderator', 'message': message}
+            ])
+        if self.author_id not in partners:
+            notifications.append([
+                (self._cr.dbname, 'res.partner', self.author_id.id),
+                {'type': 'author', 'message': message}
+            ])
+        self.env['bus.bus'].sendmany(notifications)
+
+    @api.model
+    def _notify_moderators(self):
+        """ Push a notification (Inbox/email) to moderators having messages
+        waiting for moderation. This method is called once a day by a cron.
+        """
+        channels = self.env['mail.channel'].browse(self.search([('moderation_status', '=', 'pending_moderation')]).mapped('res_id'))
+        moderators_to_notify = channels.mapped('moderator_ids')
+        template = self.env.ref('mail.mail_channel_notify_moderation', raise_if_not_found=False)
+        if not template:
+            _logger.warning('Template "mail.mail_channel_notify_moderation" was not found. Cannot send reminder notifications.')
+            return
+        MailThread = self.env['mail.thread'].with_context(mail_notify_author=True)
+        for moderator in moderators_to_notify:
+            MailThread.message_notify(
+                moderator.partner_id.ids,
+                subject=_('Message are pending moderation'),  # tocheck: target language
+                body=template.render({'record': moderator.partner_id}, engine='ir.qweb', minimal_qcontext=True),
+                email_from=moderator.company_id.catchall or moderator.company_id.email,
+            )
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 5a7169ea9f14..4cde154e1cbf 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -1294,7 +1294,7 @@ class MailThread(models.AbstractModel):
                 post_params['model'] = model
             new_msg = thread.message_post(**post_params)
 
-            if original_partner_ids:
+            if new_msg and original_partner_ids:
                 # postponed after message_post, because this is an external message and we don't want to create
                 # duplicate emails due to notifications
                 new_msg.write({'partner_ids': original_partner_ids})
@@ -1869,7 +1869,7 @@ class MailThread(models.AbstractModel):
         return m2m_attachment_ids
 
     @api.multi
-    @api.returns('self', lambda value: value.id)
+    @api.returns('mail.message', lambda value: value.id)
     def message_post(self, body='', subject=None,
                      message_type='notification', subtype=None,
                      parent_id=False, attachments=None,
@@ -1891,6 +1891,7 @@ class MailThread(models.AbstractModel):
                     to the related document. Should only be set by Chatter.
             :return int: ID of newly created mail.message
         """
+
         if attachments is None:
             attachments = {}
         if self.ids and not self.ensure_one():
@@ -1998,15 +1999,18 @@ class MailThread(models.AbstractModel):
         message and computed value are given, to try to lessen query count by
         using already-computed values instead of having to rebrowse things. """
         # Notify recipients of the newly-created message (Inbox / Email + channels)
-        message._notify(
-            layout=notif_layout,
-            force_send=self.env.context.get('mail_notify_force_send', True),
-            values=notif_values,
-        )
+        if values.get('moderation_status') != 'pending_moderation':
+            message._notify(
+                layout=notif_layout,
+                force_send=self.env.context.get('mail_notify_force_send', True),
+                values=notif_values,
+            )
 
-        # Post-process: subscribe author
-        if values['author_id'] and values['model'] and self.ids and values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
-            self._message_subscribe([values['author_id']])
+            # Post-process: subscribe author
+            if values['author_id'] and values['model'] and self.ids and values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
+                self._message_subscribe([values['author_id']])
+        else:
+            message._notify_pending_by_chat()
 
     @api.multi
     def message_post_with_view(self, views_or_xmlid, **kwargs):
diff --git a/addons/mail/models/res_company.py b/addons/mail/models/res_company.py
new file mode 100644
index 000000000000..45011902e83c
--- /dev/null
+++ b/addons/mail/models/res_company.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models, fields
+
+
+class Company(models.Model):
+    _name = 'res.company'
+    _inherit = 'res.company'
+
+    catchall = fields.Char(string="Catchall Email", compute="_compute_catchall")
+
+    @api.multi
+    def _compute_catchall(self):
+        ConfigParameter = self.env['ir.config_parameter'].sudo()
+        alias = ConfigParameter.get_param('mail.catchall.alias')
+        domain = ConfigParameter.get_param('mail.catchall.domain')
+        if alias and domain:
+            for company in self:
+                company.catchall = '%s@%s' % (alias, domain)
+        else:
+            for company in self:
+                company.catchall = ''
diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py
index f9ebd21eb1bc..11d687cbca18 100644
--- a/addons/mail/models/res_users.py
+++ b/addons/mail/models/res_users.py
@@ -11,7 +11,7 @@ class Users(models.Model):
         - make a new user follow itself
         - add a welcome message
         - add suggestion preference
-        - if adding groups to an user, check mail.channels linked to this user
+        - if adding groups to a user, check mail.channels linked to this user
           group, and the user. This is done by overriding the write method.
     """
     _name = 'res.users'
@@ -31,6 +31,39 @@ class Users(models.Model):
         help="Policy on how to handle Chatter notifications:\n"
              "- Handle by Emails: notifications are sent to your email address\n"
              "- Handle in Odoo: notifications appear in your Odoo Inbox")
+    # channel-specific: moderation
+    is_moderator = fields.Boolean(string='Is moderator', compute='_compute_is_moderator')
+    moderation_counter = fields.Integer(string='Moderation count', compute='_compute_moderation_counter')
+    moderation_channel_ids = fields.Many2many(
+        'mail.channel', 'mail_channel_moderator_rel',
+        string='Moderated channels')
+
+    @api.depends('moderation_channel_ids.moderation', 'moderation_channel_ids.moderator_ids')
+    @api.multi
+    def _compute_is_moderator(self):
+        moderated = self.env['mail.channel'].search([
+            ('id', 'in', self.mapped('moderation_channel_ids').ids),
+            ('moderation', '=', True),
+            ('moderator_ids', 'in', self.ids)
+        ])
+        user_ids = moderated.mapped('moderator_ids')
+        for user in self:
+            user.is_moderator = user in user_ids
+
+    @api.multi
+    def _compute_moderation_counter(self):
+        self._cr.execute("""
+SELECT channel_moderator.res_users_id, COUNT(msg.id)
+FROM "mail_channel_moderator_rel" AS channel_moderator
+JOIN "mail_message" AS msg
+ON channel_moderator.mail_channel_id = msg.res_id
+    AND channel_moderator.res_users_id IN %s
+    AND msg.model = 'mail.channel'
+    AND msg.moderation_status = 'pending_moderation'
+GROUP BY channel_moderator.res_users_id""", [tuple(self.ids)])
+        result = dict(self._cr.fetchall())
+        for user in self:
+            user.moderation_counter = result.get(user.id, 0)
 
     def __init__(self, pool, cr):
         """ Override of __init__ to add access rights on notification_email_send
diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv
index c9bf2e909ec6..85c2e6d4dedf 100644
--- a/addons/mail/security/ir.model.access.csv
+++ b/addons/mail/security/ir.model.access.csv
@@ -18,6 +18,7 @@ access_mail_channel_user,mail.group.user,model_mail_channel,base.group_user,1,1,
 access_mail_channel_partner_public,mail.channel.partner.public,model_mail_channel_partner,base.group_public,1,0,0,0
 access_mail_channel_partner_portal,mail.channel.partner.portal,model_mail_channel_partner,base.group_portal,1,1,1,1
 access mail_channel_partner_user,mail.channel.partner.user,model_mail_channel_partner,base.group_user,1,1,1,1
+access_mail_moderation_user,mail.moderation.user,model_mail_moderation,base.group_user,1,1,1,1
 access_mail_alias_all,mail.alias.all,model_mail_alias,,1,0,0,0
 access_mail_alias_user,mail.alias.user,model_mail_alias,base.group_user,1,1,1,1
 access_mail_alias_system,mail.alias.system,model_mail_alias,base.group_system,1,1,1,1
diff --git a/addons/mail/security/mail_security.xml b/addons/mail/security/mail_security.xml
index b9b13d9de3bc..d6d335786646 100644
--- a/addons/mail/security/mail_security.xml
+++ b/addons/mail/security/mail_security.xml
@@ -51,5 +51,10 @@
             <field name="perm_unlink" eval="True"/>
         </record>
 
+        <record id="mail_moderation_rule_user" model="ir.rule">
+            <field name="name">White/Black List: moderators: moderated channels only</field>
+            <field name="model_id" ref="model_mail_moderation"/>
+            <field name="domain_force">[('channel_id.moderator_ids', 'in', user.id)]</field>
+        </record>
     </data>
 </odoo>
diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js
index 3f37a620c80b..def90b01595c 100644
--- a/addons/mail/static/src/js/discuss.js
+++ b/addons/mail/static/src/js/discuss.js
@@ -106,10 +106,73 @@ var PartnerInviteDialog = Dialog.extend({
     },
 });
 
+/**
+ * Widget : Moderator reject message dialog
+ *
+ * Popup containing message title and reject message body.
+ * This let the moderator provide a reason for rejecting the messages.
+ */
+var ModeratorRejectMessageDialog = Dialog.extend({
+    template: "mail.ModeratorRejectMessageDialog",
+
+    /**
+     * @override
+     * @param {web.Widget} parent
+     * @param {Object} params
+     * @param {integer[]} params.messageIDs list of message IDs to send
+     *   'reject' decision reason
+     * @param {function} params.proceedReject a function to call when the
+     *   moderator confirms the reason for rejecting the messages. This
+     *   function passes an object as the reason for reject, which is
+     *   structured as follow:
+     *
+     *          {
+     *              title: <string>,
+     *              comment: <string>,
+     *          }
+     */
+    init: function (parent, params) {
+        this._messageIDs = params.messageIDs;
+        this._proceedReject = params.proceedReject;
+        this._super(parent, {
+            title: _t('Send explanation to author'),
+            size: "medium",
+            buttons: [{
+                text: _t("Send"),
+                close: true,
+                classes: "btn-primary",
+                click: _.bind(this._onSendClicked, this),
+            }],
+        });
+    },
+    //--------------------------------------------------------------------------
+    // Handlers
+    //--------------------------------------------------------------------------
+
+    /**
+     * Called when the moderator would like to submit reason for rejecting the
+     * messages.
+     *
+     * @private
+     */
+    _onSendClicked: function () {
+        var title = this.$('#message_title').val();
+        var comment = this.$('#reject_message').val();
+        if (title && comment) {
+            this._proceedReject({
+                title: title,
+                comment: comment
+            });
+        }
+    },
+});
+
 var Discuss = AbstractAction.extend(ControlPanelMixin, {
     template: 'mail.discuss',
     custom_events: {
+        message_moderation: '_onMessageModeration',
         search: '_onSearch',
+        update_moderation_buttons: '_onUpdateModerationButtons',
     },
     events: {
         'blur .o_mail_add_channel input': '_onAddChannelBlur',
@@ -239,6 +302,49 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
     // Private
     //--------------------------------------------------------------------------
 
+    /**
+     * Ban the authors of the messages with ID in `messageIDs`
+     * Show a confirmation dialog to the moderator.
+     *
+     * @private
+     * @param {integer[]} messageIDs IDs of messages for which we should ban authors
+     */
+    _banAuthorsFromMessageIDs: function (messageIDs) {
+        var self = this;
+        var emailList = _.map(messageIDs, function (messageID) {
+            return self.call('chat_manager', 'getMessage', messageID).email_from;
+        }).join(", ");
+        var text = _.str.sprintf(_t("You are going to ban: %s. Do you confirm the action?"), emailList);
+        var options = {
+            confirm_callback: function () {
+                self._moderateMessages(messageIDs, 'ban');
+            }
+        };
+        Dialog.confirm(this, text, options);
+    },
+    /**
+     * Discard the messages with ID in `messageIDs`
+     * Show a confirmation dialog to the moderator.
+     *
+     * @private
+     * @param {integer[]]} messageIDs list of message IDs to discard
+     */
+    _discardMessages: function (messageIDs) {
+        var self = this;
+        var num = messageIDs.length;
+        var text;
+        if (num > 1) {
+            text = _.str.sprintf(_t("You are going to discard %s messages. Do you confirm the action?"), num);
+        } else if (num === 1) {
+            text = _t("You are going to discard 1 message. Do you confirm the action?");
+        }
+        var options = {
+            confirm_callback: function () {
+                self._moderateMessages(messageIDs, 'discard');
+            }
+        };
+        Dialog.confirm(this, text, options);
+    },
     /**
      * @private
      * @returns {$.Promise}
@@ -278,11 +384,35 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
             squash_close_messages: this.channel.type !== 'static' && !this.channel.mass_mailing,
             display_empty_channel: !messages.length && !this.domain.length,
             display_no_match: !messages.length && this.domain.length,
-            display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox",
+            display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox" || this.channel.id === "channel_moderation",
             display_email_icon: false,
             display_reply_icon: true,
         };
     },
+    /**
+     * Determine the action to apply on messages with ID in `messageIDs`
+     * based on the moderation decision `decision`.
+     *
+     * @private
+     * @param {number[]} messageIDs list of message ids that are moderated
+     * @param {string} decision of the moderator, could be either 'reject',
+     *   'discard', 'ban', 'accept', 'allow'.
+     */
+     _handleModerationDecision: function (messageIDs, decision) {
+        if (messageIDs) {
+            if (decision === 'reject') {
+                this._rejectMessages(messageIDs);
+            } else if (decision === 'discard') {
+                this._discardMessages(messageIDs);
+            } else if (decision === 'ban') {
+                this._banAuthorsFromMessageIDs(messageIDs);
+            } else {
+                // other decisions do not need more information,
+                // confirmation dialog, etc.
+                this._moderateMessages(messageIDs, decision);
+            }
+        }
+     },
     /**
      * Ensures that enough messages have been loaded to fill the entire screen
      * (this is particularily important because remaining messages can only be
@@ -325,6 +455,33 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
                 self.thread.scroll_to({offset: offset});
             });
     },
+    /**
+     * Apply the moderation decision `decision` on the messages with ID in
+     * `messageIDs`.
+     *
+     * @private
+     * @param {integer[]} messageIDs list of message IDs to apply the
+     *   moderation decision.
+     * @param {string} decision the moderation decision to apply on the
+     *   messages. Could be either 'reject', 'discard', 'ban', 'accept',
+     *   or 'allow'.
+     * @param {Object|undefined} [kwargs] optional data to pass on
+     *   message moderation. This is provided when rejecting the messages
+     *   for which title and comment give reason(s) for reject.
+     * @param {string} [kwargs.title]
+     * @param {string} [kwargs.comment]
+     * @return {undefined|$.Promise}
+     */
+    _moderateMessages: function (messageIDs, decision, kwargs) {
+        if (messageIDs.length && decision) {
+            return this._rpc({
+                model: 'mail.message',
+                method: 'moderate',
+                args: [messageIDs, decision],
+                kwargs: kwargs
+            });
+        }
+    },
     /**
      * Binds handlers on the given $input to make them autocomplete and/or
      * create channels.
@@ -393,6 +550,24 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
             });
         }
     },
+    /**
+     * Reject the messages
+     *
+     * The moderator must provide a reason for reject, and may also
+     * cancel his action.
+     *
+     * @private
+     * @param {number[]]} messageIDs list of message IDs to reject
+     */
+    _rejectMessages: function (messageIDs) {
+        var self = this;
+        new ModeratorRejectMessageDialog(this, {
+            messageIDs: messageIDs,
+            proceedReject: function (reason) {
+                self._moderateMessages(messageIDs, 'reject', reason);
+            }
+        }).open();
+    },
     /**
      * @private
      */
@@ -402,6 +577,9 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
         this.$buttons.on('click', '.o_mail_chat_button_invite', this._onInviteButtonClicked.bind(this));
         this.$buttons.on('click', '.o_mail_chat_button_mark_read', this._onMarkAllReadClicked.bind(this));
         this.$buttons.on('click', '.o_mail_chat_button_unstar_all', this._onUnstarAllClicked.bind(this));
+        this.$buttons.on('click', '.o_mail_chat_button_moderate_all', this._onModerateAllClicked.bind(this));
+        this.$buttons.on('click', '.o_mail_chat_button_select_all', this._onSelectAllClicked.bind(this));
+        this.$buttons.on('click', '.o_mail_chat_button_unselect_all', this._onUnselectAllClicked.bind(this));
     },
     /**
      * @private
@@ -454,25 +632,23 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
      * @returns {Deferred}
      */
     _renderThread: function () {
-        var self = this;
         this.thread = new ChatThread(this, {display_help: true, loadMoreOnScroll: true});
 
         this.thread.on('redirect', this, function (resModel, resID) {
-            self.call('chat_manager', 'redirect', resModel, resID, self._setChannel.bind(self));
+            this.call('chat_manager', 'redirect', resModel, resID, this._setChannel.bind(this));
         });
         this.thread.on('redirect_to_channel', this, function (channelID) {
-            self.call('chat_manager', 'joinChannel', channelID).then(this._setChannel.bind(this));
+            this.call('chat_manager', 'joinChannel', channelID).then(this._setChannel.bind(this));
         });
         this.thread.on('load_more_messages', this, this._loadMoreMessages);
         this.thread.on('mark_as_read', this, function (messageID) {
-            self.call('chat_manager', 'markAsRead', [messageID]);
+            this.call('chat_manager', 'markAsRead', [messageID]);
         });
         this.thread.on('toggle_star_status', this, function (messageID) {
-            self.call('chat_manager', 'toggleStarStatus', messageID);
+            this.call('chat_manager', 'toggleStarStatus', messageID);
         });
         this.thread.on('select_message', this, this._selectMessage);
         this.thread.on('unselect_message', this, this._unselectMessage);
-
         return this.thread.appendTo(this.$('.o_mail_chat_content'));
     },
     /**
@@ -612,6 +788,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
         chatBus.on('update_channel_unread_counter', this, this.throttledUpdateChannels);
         chatBus.on('update_dm_presence', this, this.throttledUpdateChannels);
         chatBus.on('activity_updated', this, this.throttledUpdateChannels);
+        chatBus.on('update_moderation_counter', this, this.throttledUpdateChannels);
     },
     /**
      * Stores the scroll position and composer state of the current channel
@@ -663,6 +840,8 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
             channels: this.call('chat_manager', 'getChannels'),
             needaction_counter: this.call('chat_manager', 'getNeedactionCounter'),
             starred_counter: this.call('chat_manager', 'getStarredCounter'),
+            moderationCounter: this.call('chat_manager', 'getModerationCounter'),
+            isModerator: this.call('chat_manager', 'isModerator'),
         });
         this.$(".o_mail_chat_sidebar").html($sidebar.contents());
         _.each(['dm', 'public', 'private'], function (type) {
@@ -694,6 +873,9 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
                 .find('.o_mail_chat_button_unstar_all')
                 .toggleClass('disabled', disabled);
         }
+        if ((this.channel.isModerated && this.channel.isModerator) || this.channel.id === "channel_moderation") {
+            this._updateModerationButtons();
+        }
     },
     /**
      * @private
@@ -727,13 +909,69 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
             .find('.o_mail_chat_button_unstar_all')
             .toggle(channel.id === "channel_starred")
             .removeClass("o_hidden");
-
+        this.$buttons
+            .find('.o_mail_chat_button_select_all')
+            .toggle((channel.isModerated && channel.isModerator) || channel.id === "channel_moderation")
+            .removeClass("o_hidden");
+        this.$buttons
+            .find('.o_mail_chat_button_unselect_all')
+            .toggle((channel.isModerated && channel.isModerator) || channel.id === "channel_moderation")
+            .removeClass("o_hidden");
+        this.$buttons.find('.o_mail_chat_button_moderate_all').hide();
         this.$('.o_mail_chat_channel_item')
             .removeClass('o_active')
             .filter('[data-channel-id=' + channel.id + ']')
             .removeClass('o_unread_message')
             .addClass('o_active');
     },
+    /**
+     * Update the moderation buttons.
+     *
+     * @private
+     */
+    _updateModerationButtons: function () {
+        this._updateSelectUnselectAllButtons();
+        this._updateModerationDecisionButton();
+    },
+    /**
+     * Display/hide the "moderate all" button based on whether
+     * some moderation checkboxes are checked or not.
+     * If some checkboxes are checked, display this button,
+     * otherwise hide it.
+     *
+     * @private
+     */
+    _updateModerationDecisionButton: function () {
+        if (this.thread.$('.moderation_checkbox:checked').length) {
+            this.$buttons.find('.o_mail_chat_button_moderate_all').show();
+        } else {
+            this.$buttons.find('.o_mail_chat_button_moderate_all').hide();
+        }
+    },
+    /**
+     * @private
+     */
+    _updateSelectUnselectAllButtons: function () {
+        var buttonSelect = this.$buttons.find('.o_mail_chat_button_select_all');
+        var buttonUnselect = this.$buttons.find('.o_mail_chat_button_unselect_all');
+        var numCheckboxes = this.thread.$('.moderation_checkbox').length;
+        var numCheckboxesChecked = this.thread.$('.moderation_checkbox:checked').length;
+        if (numCheckboxes) {
+            if (numCheckboxesChecked === numCheckboxes) {
+                buttonSelect.toggleClass('disabled', true);
+                buttonUnselect.toggleClass('disabled', false);
+            } else if (numCheckboxesChecked === 0) {
+                buttonSelect.toggleClass('disabled', false);
+                buttonUnselect.toggleClass('disabled', true);
+            } else {
+                buttonSelect.toggleClass('disabled', false);
+                buttonUnselect.toggleClass('disabled', false);
+            }
+        } else {
+            buttonSelect.toggleClass('disabled', true);
+            buttonUnselect.toggleClass('disabled', true);
+        }
+    },
 
     //--------------------------------------------------------------------------
     // Handlers
@@ -778,6 +1016,12 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
         this._updateChannels();
         delete this.channelsScrolltop[channelID];
     },
+    /**
+     * @private
+     */
+    _onCloseNotificationBar: function () {
+        this.$(".o_mail_annoying_notification_bar").slideUp();
+    },
     /**
      * @private
      */
@@ -788,6 +1032,22 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
         composer.mention_set_enabled_commands(commands);
         composer.mention_set_prefetched_partners(partners);
     },
+    /**
+     * @private
+     */
+    _onInviteButtonClicked: function () {
+        var title = _.str.sprintf(_t('Invite people to #%s'), this.channel.name);
+        new PartnerInviteDialog(this, title, this.channel.id).open();
+    },
+    /**
+     * @private
+     * @param {KeyEvent} event
+     */
+    _onKeydown: function (event) {
+        if (event.which === $.ui.keyCode.ESCAPE && this.selected_message) {
+            this._unselectMessage();
+        }
+    },
     /**
      * @private
      */
@@ -803,6 +1063,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
         var self = this;
         var currentChannelID = this.channel.id;
         if ((currentChannelID === "channel_starred" && !message.is_starred) ||
+            (currentChannelID === "channel_moderation" && !message.needsModeration) ||
             (currentChannelID === "channel_inbox" && !message.is_needaction)) {
                 this.call('chat_manager', 'getMessages', {
                         channelID: this.channel.id,
@@ -814,10 +1075,34 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
                             self._updateButtonStatus(messages.length === 0, type);
                         });
                 });
-        } else if (_.contains(message.channel_ids, currentChannelID)) {
+        } else if (_.contains(message.channel_ids, currentChannelID) || (message.res_id === currentChannelID)) {
             this._fetchAndRenderThread();
         }
     },
+    /**
+     * @private
+     * @param {MouseEvent} ev
+     */
+    _onModerateAllClicked: function (ev) {
+        var decision = $(ev.target).data('decision');
+        var messageIDs = this.thread.$('.moderation_checkbox:checked')
+                            .map(function () {
+                                return $(this).data('message-id');
+                            })
+                            .get();
+        this._handleModerationDecision(messageIDs, decision);
+    },
+    /**
+     * @private
+     * @param {OdooEvent} ev
+     * @param {integer} ev.data.messageID ID of the moderated message
+     * @param {string} ev.data.decision can be 'reject', 'discard', 'ban', 'accept', 'allow'.
+     */
+    _onMessageModeration: function (ev) {
+        var messageIDs = [ev.data.messageID];
+        var decision = ev.data.decision;
+        this._handleModerationDecision(messageIDs, decision);
+    },
     /**
      * @private
      * @param {Object} channel
@@ -872,7 +1157,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
                 }
             })
             .fail(function () {
-                // todo: display notification
+                // todo: display notifications
             });
     },
     /**
@@ -889,28 +1174,6 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
             on_reverse_breadcrumb: this.on_reverse_breadcrumb,
         });
     },
-    /**
-     * @private
-     */
-    _onCloseNotificationBar: function () {
-        this.$(".o_mail_annoying_notification_bar").slideUp();
-    },
-    /**
-     * @private
-     */
-    _onInviteButtonClicked: function () {
-        var title = _.str.sprintf(_t('Invite people to #%s'), this.channel.name);
-        new PartnerInviteDialog(this, title, this.channel.id).open();
-    },
-    /**
-     * @private
-     * @param {KeyEvent} event
-     */
-    _onKeydown: function (event) {
-        if (event.which === $.ui.keyCode.ESCAPE && this.selected_message) {
-            this._unselectMessage();
-        }
-    },
     /**
      * @private
      * @param {MouseEvent} event
@@ -950,6 +1213,17 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
             // this will be done as soon as the default channel is set
             this._fetchAndRenderThread();
         }
+    },
+     /**
+     * @private
+     * @param {MouseEvent} ev
+     */
+    _onSelectAllClicked: function (ev) {
+        var $button = $(ev.target);
+        if (!$button.hasClass('disabled')) {
+            this.thread.toggleModerationCheckboxes(true);
+            this._updateModerationButtons();
+        }
     },
     /**
      * @private
@@ -975,12 +1249,33 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
         var channel = this.call('chat_manager', 'getChannel', channelID);
         this.call('chat_manager', 'unsubscribe', channel);
     },
+    /**
+     * @private
+     * @param {MouseEvent} ev
+     */
+    _onUnselectAllClicked: function (ev) {
+        var $button = $(ev.target);
+        if (!$button.hasClass('disabled')) {
+            this.thread.toggleModerationCheckboxes(false);
+            this._updateModerationButtons();
+        }
+    },
     /**
      * @private
      */
     _onUnstarAllClicked: function () {
         this.call('chat_manager', 'unstarAll');
     },
+    /**
+     * Update the moderation buttons.
+     * This is triggered when a moderation checkbox
+     * has its checked property changed.
+     *
+     * @private
+     */
+    _onUpdateModerationButtons: function () {
+        this._updateModerationButtons();
+    },
 });
 
 core.action_registry.add('mail.chat.instant_messaging', Discuss);
diff --git a/addons/mail/static/src/js/services/chat_manager.js b/addons/mail/static/src/js/services/chat_manager.js
index 4bb6e9be6aab..eaedb9cd11e8 100644
--- a/addons/mail/static/src/js/services/chat_manager.js
+++ b/addons/mail/static/src/js/services/chat_manager.js
@@ -125,6 +125,8 @@ var ChatManager =  AbstractService.extend({
         this.emojis = [];
         this.needactionCounter = 0;
         this.starredCounter = 0;
+        this.moderationCounter = 0;
+        this.moderatedChannelIDs = [];
         this.mentionPartnerSuggestions = [];
         this.cannedResponses = [];
         this.commands = [];
@@ -521,6 +523,14 @@ var ChatManager =  AbstractService.extend({
         }
         return $.when([]);
     },
+    /**
+     * Returns the number of messages that the user needs to moderate
+     *
+     * @return {integer}
+     */
+    getModerationCounter: function () {
+        return this.moderationCounter;
+    },
     /**
      * Returns the number of messages received from followed channels
      * + all messages where the current user is notified.
@@ -556,6 +566,12 @@ var ChatManager =  AbstractService.extend({
     isAllHistoryLoaded: function (channel, domain) {
         return this._getChannelCache(channel, domain).all_history_loaded;
     },
+    /**
+     * @return {boolean}
+     */
+    isModerator: function () {
+        return this._isModerator;
+    },
     /**
      * @return {$.Promise}
      */
@@ -733,7 +749,7 @@ var ChatManager =  AbstractService.extend({
                         subtype: 'mail.mt_comment',
                         command: data.command,
                     }),
-                });
+                 });
         }
         if ('model' in options && 'res_id' in options) {
             // post a message in a chatter
@@ -1034,6 +1050,15 @@ var ChatManager =  AbstractService.extend({
             }
         } else if (options.domain && options.domain !== []) {
             this._addToCache(msg, options.domain);
+        } else if (data.moderation_status === 'accepted') {
+            msg.channel_ids = data.channel_ids;
+            msg.needsModeration = false;
+            if (msg.isModerator) {
+                this.moderationCounter--;
+                this._removeMessageFromChannel("channel_moderation", msg);
+                this.chatBus.trigger('update_moderation_counter');
+            }
+            this.chatBus.trigger('update_message', msg);
         }
         return msg;
     },
@@ -1110,9 +1135,16 @@ var ChatManager =  AbstractService.extend({
         var self = this;
         options = options || {};
         var domain =
-            (channel.id === "channel_inbox") ? [['needaction', '=', true]] :
-            (channel.id === "channel_starred") ? [['starred', '=', true]] :
-                                                [['channel_ids', 'in', channel.id]];
+            (channel.id === 'channel_inbox') ? [['needaction', '=', true]] :
+            (channel.id === 'channel_starred') ? [['starred', '=', true]] :
+            (channel.id === 'channel_moderation') ? [['need_moderation', '=', true]] :
+                                                    ['|',
+                                                     '&', '&',
+                                                     ['model', '=', 'mail.channel'],
+                                                     ['res_id', 'in', [channel.id]],
+                                                     ['need_moderation', '=', true],
+                                                     ['channel_ids', 'in', [channel.id]]
+                                                    ];
         var cache = this._getChannelCache(channel, options.domain);
 
         if (options.domain) {
@@ -1185,6 +1217,18 @@ var ChatManager =  AbstractService.extend({
             });
             self.needactionCounter = result.needaction_inbox_counter || 0;
             self.starredCounter = result.starred_counter || 0;
+            self.moderationCounter = result.moderation_counter;
+            self.moderatedChannelIDs = result.moderation_channel_ids;
+            self._isModerator = result.is_moderator;
+
+            //if user is moderator then add moderation channel
+            if (self._isModerator) {
+                self._addChannel({
+                    id: "channel_moderation",
+                    name: _lt("Moderate Messages"),
+                    type: "static"
+                });
+            }
             self.commands = _.map(result.commands, function (command) {
                 return _.extend({ id: command.name }, command);
             });
@@ -1233,17 +1277,19 @@ var ChatManager =  AbstractService.extend({
      * @param  {Object[]} [data.direct_partner]
      * @param  {boolean} [data.group_based_subscription]
      * @param  {integer|string} data.id
+     * @param  {boolean} data.is_moderator
      * @param  {boolean} data.is_minimized
      * @param  {string} [data.last_message_date]
      * @param  {boolean} data.mass_mailing
      * @param  {integer} [data.message_needaction_counter]
      * @param  {integer} [data.message_unread_counter]
+     * @param  {boolean} data.moderation whether this channel is moderated or not
      * @param  {string} data.name
      * @param  {string} [data.public]
      * @param  {integer} data.seen_message_id
      * @param  {string} [data.state]
      * @param  {string} [data.type]
-     * @param  {string} channel.uuid
+     * @param  {string} data.uuid
      * @param  {Object} options
      * @param  {boolean} [options.autoswitch]
      * @param  {boolean} options.displayNeedactions
@@ -1264,6 +1310,8 @@ var ChatManager =  AbstractService.extend({
             hidden: options.hidden,
             display_needactions: options.displayNeedactions,
             mass_mailing: data.mass_mailing,
+            isModerated: data.moderation,
+            isModerator: data.is_moderator,
             group_based_subscription: data.group_based_subscription,
             needaction_counter: data.message_needaction_counter || 0,
             unread_counter: 0,
@@ -1314,6 +1362,7 @@ var ChatManager =  AbstractService.extend({
      * @param  {boolean} data.is_note
      * @param  {boolean} data.is_notification
      * @param  {string} data.message_type
+     * @param  {string} [data.moderation_status]
      * @param  {string} [data.model]
      * @param  {boolean} data.module_icon src url of the module icon
      * @param  {string} data.record_name
@@ -1333,6 +1382,9 @@ var ChatManager =  AbstractService.extend({
             message_type: data.message_type,
             subtype_description: data.subtype_description,
             is_author: data.author_id && data.author_id[0] === session.partner_id,
+            isModerator: data.model === 'mail.channel' &&
+                _.contains(this.moderatedChannelIDs, data.res_id) &&
+                data.moderation_status === 'pending_moderation',
             is_note: data.is_note,
             is_discussion: data.is_discussion,
             is_notification: data.is_notification,
@@ -1384,6 +1436,7 @@ var ChatManager =  AbstractService.extend({
         Object.defineProperties(msg, {
             is_starred: propertyDescr("channel_starred"),
             is_needaction: propertyDescr("channel_inbox"),
+            needsModerationByUser: propertyDescr("channel_moderation"),
         });
 
         if (_.contains(data.needaction_partner_ids, session.partner_id)) {
@@ -1392,8 +1445,17 @@ var ChatManager =  AbstractService.extend({
         if (_.contains(data.starred_partner_ids, session.partner_id)) {
             msg.is_starred = true;
         }
+        if (data.moderation_status === 'pending_moderation') {
+            msg.needsModeration = true;
+            msg.needsModerationByUser = msg.isModerator;
+            // the message is not linked to the moderated channel on the
+            // server, therefore this message has not this channel in
+            // channel_ids. Here, just to show this message in the channel
+            //visually, it links this message to the channel
+            msg.channel_ids.push(msg.res_id);
+        }
         if (msg.model === 'mail.channel') {
-            var realChannels = _.without(msg.channel_ids, 'channel_inbox', 'channel_starred');
+            var realChannels = _.without(msg.channel_ids, 'channel_inbox', 'channel_starred', 'channel_moderation');
             var origin = realChannels.length === 1 ? realChannels[0] : undefined;
             var channel = origin && this.getChannel(origin);
             if (channel) {
@@ -1467,6 +1529,13 @@ var ChatManager =  AbstractService.extend({
     _manageActivityUpdateNotification: function (data) {
         this.chatBus.trigger('activity_updated', data);
     },
+    /**
+     * @private
+     * @param {Object} data
+     */
+    _manageAuthorNotification: function (data) {
+        this._addMessage(data.message);
+    },
     /**
      * @private
      * @param  {Object} message
@@ -1545,6 +1614,27 @@ var ChatManager =  AbstractService.extend({
             }
         }
     },
+    /**
+     * @private
+     * @param {Object} data
+     * @param {Object[]} [data.message_ids]
+     */
+    _manageDeletionNotification: function (data) {
+        var self = this;
+        _.each(data.message_ids, function (msgID) {
+            var message = _.findWhere(self.messages, { id: msgID });
+            if (message) {
+                if (message.isModerator) {
+                    self._removeMessageFromChannel("channel_moderation", message);
+                    self.moderationCounter--;
+                }
+                message.needsModeration = false;
+                self._removeMessageFromChannel(message.res_id, message);
+                self.chatBus.trigger('update_message', message);
+                }
+            });
+        this.chatBus.trigger('update_moderation_counter');
+    },
     /**
      * Updates channel_inbox when a message has marked as read.
      *
@@ -1579,6 +1669,16 @@ var ChatManager =  AbstractService.extend({
         this.needactionCounter = Math.max(this.needactionCounter - data.message_ids.length, 0);
         this.chatBus.trigger('update_needaction', this.needactionCounter);
     },
+    /**
+     * @private
+     * @param {Object} data notification data
+     * @param {Object} data.message
+     */
+    _manageModeratorNotification: function (data) {
+        this.moderationCounter++;
+        this._addMessage(data.message);
+        this.chatBus.trigger('update_moderation_counter');
+    },
     /**
      * @private
      * @param  {Object} message
@@ -1626,6 +1726,12 @@ var ChatManager =  AbstractService.extend({
             this._manageToggleStarNotification(data);
         } else if (data.type === 'mark_as_read') {
             this._manageMarkAsReadNotification(data);
+        } else if (data.type === 'moderator') {
+            this._manageModeratorNotification(data);
+        } else if (data.type === 'author') {
+            this._manageAuthorNotification(data);
+        } else if (data.type === 'deletion') {
+            this._manageDeletionNotification(data);
         } else if (data.info === 'channel_seen') {
             this._manageChannelSeenNotification(data);
         } else if (data.info === 'transient_message') {
diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js
index f34823ff9fc9..39d179a715a3 100644
--- a/addons/mail/static/src/js/thread.js
+++ b/addons/mail/static/src/js/thread.js
@@ -38,6 +38,8 @@ var Thread = Widget.extend({
             var message_id = $(event.currentTarget).data('message-id');
             this.trigger("mark_as_read", message_id);
         },
+        "click .o_thread_message_moderation": "_onClickMessageModeration",
+        "change .moderation_checkbox": "_onChangeModerationCheckbox",
         "click .o_thread_message_star": function (event) {
             var message_id = $(event.currentTarget).data('message-id');
             this.trigger("toggle_star_status", message_id);
@@ -141,7 +143,6 @@ var Thread = Widget.extend({
             }
             prev_msg = msg;
         });
-
         this.$el.html(QWeb.render('mail.ChatThread', {
             messages: msgs,
             options: options,
@@ -313,6 +314,15 @@ var Thread = Widget.extend({
     destroy: function () {
         clearInterval(this.update_timestamps_interval);
     },
+    /**
+     * Toggle all the moderation checkboxes in the thread
+     *
+     * @param {boolean} checked if true, check the boxes,
+     *      otherwise uncheck them.
+     */
+    toggleModerationCheckboxes: function (checked) {
+        this.$('.moderation_checkbox').prop('checked', checked);
+    },
 
     //--------------------------------------------------------------------------
     // Private
@@ -387,6 +397,23 @@ var Thread = Widget.extend({
             attachmentViewer.appendTo($('body'));
         }
     },
+    /**
+     * @private
+     * @param {MouseEvent} ev
+     */
+    _onChangeModerationCheckbox: function (ev) {
+        this.trigger_up('update_moderation_buttons');
+    },
+    /**
+     * @private
+     * @param {MouseEvent} ev
+     */
+     _onClickMessageModeration: function (ev) {
+        var $button = $(ev.currentTarget);
+        var messageID = $button.data('message-id');
+        var decision = $button.data('decision');
+        this.trigger_up('message_moderation', { messageID: messageID, decision: decision });
+    },
 });
 
 Thread.ORDER = ORDER;
diff --git a/addons/mail/static/src/xml/discuss.xml b/addons/mail/static/src/xml/discuss.xml
index b0a3bfe1ba95..0bfec270eea7 100644
--- a/addons/mail/static/src/xml/discuss.xml
+++ b/addons/mail/static/src/xml/discuss.xml
@@ -29,6 +29,12 @@
                 <t t-set="counter" t-value="starred_counter"/>
                 <t t-call="mail.chat.SidebarNeedaction"/>
             </div>
+            <div t-if="isModerator" t-attf-class="o_mail_chat_title_main o_mail_chat_channel_item #{(active_channel_id == 'channel_moderation') ? 'o_active': ''}"
+                 data-channel-id="channel_moderation">
+                <span class="o_channel_name"> <i class="fa fa-envelope mr8"/>Moderation Queue</span>
+                <t t-set="counter" t-value="moderationCounter"/>
+                <t t-call="mail.chat.SidebarNeedaction"/>
+            </div>
             <hr class="mb8"/>
 
             <t t-set="channel_type" t-value="'public'"/>
@@ -137,6 +143,11 @@
             <button type="button" class="btn btn-default btn-sm o_mail_chat_button_unstar_all" title="Unstar all messages">Unstar all</button>
             <button type="button" class="btn btn-default btn-sm o_mail_chat_button_dm visible-xs" title="New Message">New Message</button>
             <button t-if="!disable_add_channel" type="button" class="btn btn-default btn-sm o_mail_chat_button_public o_mail_chat_button_private visible-xs" title="New Channel">New Channel</button>
+            <button type="button" class="btn btn-default btn-sm o_hidden o_mail_chat_button_select_all" title="Select all messages to moderate">Select All</button>
+            <button type="button" class="btn btn-default btn-sm o_hidden o_mail_chat_button_unselect_all" title="Unselect all messages to moderate">Unselect All</button>
+            <button type="button" data-decision="accept" class="btn btn-default btn-sm o_mail_chat_button_moderate_all" title="Accept selected messages">Accept</button>
+            <button type="button" data-decision="reject" class="btn btn-default btn-sm o_mail_chat_button_moderate_all" title="Reject selected messages">Reject</button>
+            <button type="button" data-decision="discard" class="btn btn-default btn-sm o_mail_chat_button_moderate_all" title="Discard selected messages">Discard</button>
         </div>
     </t>
 
@@ -145,6 +156,10 @@
         <input type="text" class="o_input o_mail_chat_partner_invite_input" id="mail_search_partners"/>
     </div>
 
+    <div t-name="mail.ModeratorRejectMessageDialog">
+        <input class="form-control" type="text" id="message_title" placeholder="Subject" value="Message Rejected"/>
+        <textarea class="form-control mt16" id="reject_message" placeholder="Mail Body">Your message was rejected by moderator.</textarea>
+    </div>
 
     <!-- Mobile templates -->
     <t t-name="mail.discuss_mobile">
diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml
index 5ef23d46c43f..20049e89cf6a 100644
--- a/addons/mail/static/src/xml/thread.xml
+++ b/addons/mail/static/src/xml/thread.xml
@@ -103,6 +103,10 @@
             <div class="o_thread_title">No starred message</div>
             <div>You can mark any message as 'starred', and it shows up in this channel.</div>
         </t>
+        <t t-if="options.channel_id==='channel_moderation'">
+            <div class="o_thread_title">You have no message to moderate</div>
+            <div>Pending moderation messages appear here.</div>
+        </t>
     </div>
 
     <t t-name="mail.ChatThread.Content">
@@ -144,6 +148,7 @@
             </div>
             <div class="o_thread_message_core">
                 <p t-if="message.display_author" class="o_mail_info text-muted">
+                    <input t-if="message.needsModeration and message.isModerator" type="checkbox" class="moderation_checkbox" t-att-data-message-id="message.id"/>
                     <t t-if="message.is_note">
                         Note by
                     </t>
@@ -185,7 +190,7 @@
                         </span>
                     </span>
                     <span t-attf-class="o_thread_icons">
-                        <i t-if="options.display_stars &amp;&amp; !message.is_system_notification"
+                        <i t-if="options.display_stars and !message.is_system_notification and !message.needsModeration"
                             t-att-class="'fa fa-lg o_thread_icon o_thread_message_star ' + (message.is_starred ? 'fa-star' : 'fa-star-o')"
                             t-att-data-message-id="message.id" title="Mark as Todo"/>
                        <i t-if="message.record_name &amp;&amp; message.model != 'mail.channel' &amp;&amp; options.display_reply_icon"
@@ -194,6 +199,15 @@
                         <i t-if="message.is_needaction &amp;&amp; options.display_needactions"
                            class="fa fa-check o_thread_icon o_thread_message_needaction"
                            t-att-data-message-id="message.id" title="Mark as Read"/>
+
+                        <t t-if="message.needsModeration and message.model === 'mail.channel'">
+                            <t t-if="message.isModerator">
+                                <t t-call="mail.MessageModerationDecision"/>
+                            </t>
+                            <t t-elif="message.is_author">
+                                <t t-call="mail.MessageModerationFeedback"/>
+                            </t>
+                        </t>
                     </span>
                 </p>
                 <div class="o_thread_message_content">
@@ -227,6 +241,26 @@
         </t>
     </t>
 
+    <t t-name="mail.MessageModerationDecision">
+        <i class="o_thread_icon ml4 o_thread_message_moderation text-success"
+           t-att-data-message-id="message.id" data-decision="accept" title="Accept"><b>Accept |</b></i>
+        <i class="o_thread_icon ml4 o_thread_message_moderation text-danger"
+           t-att-data-message-id="message.id" data-decision="reject" title="Remove message with explanation"><b>Reject |</b></i>
+        <i class="o_thread_icon ml4 o_thread_message_moderation text-danger"
+           t-att-data-message-id="message.id" data-decision="discard" title="Remove message without explanation"><b>Discard |</b></i>
+        <i class="o_thread_icon ml4 o_thread_message_moderation text-success"
+           t-att-data-message-id="message.id" data-decision="allow" title="Add this email address to white list of people"><b>Always Allow |</b></i>
+        <i class="o_thread_icon ml4 o_thread_message_moderation text-danger"
+           t-att-data-message-id="message.id" data-decision="ban" title="Ban this email address"><b>Ban</b></i>
+    </t>
+
+    <t t-name="mail.MessageModerationFeedback">
+        <i class="text-danger" title="Your message is pending moderation"><b>Pending moderation</b></i>
+    </t>
+
+
+
+
     <t t-name="mail.MessagesSeparator">
         <div class="o_thread_new_messages_separator">
             <span class="o_thread_separator_label">New messages</span>
diff --git a/addons/mail/static/tests/discuss_moderation_tests.js b/addons/mail/static/tests/discuss_moderation_tests.js
new file mode 100644
index 000000000000..da8e8b1c68db
--- /dev/null
+++ b/addons/mail/static/tests/discuss_moderation_tests.js
@@ -0,0 +1,822 @@
+odoo.define('mail.discuss_moderation_tests', function (require) {
+"use strict";
+
+var ChatManager = require('mail.ChatManager');
+var mailTestUtils = require('mail.testUtils');
+
+var Bus = require('web.Bus');
+
+var createBusService = mailTestUtils.createBusService;
+var createDiscuss = mailTestUtils.createDiscuss;
+
+QUnit.module('mail', {}, function () {
+
+QUnit.module('Discuss moderation', {
+    beforeEach: function () {
+        // patch _.debounce and _.throttle to be fast and synchronous
+        this.underscoreDebounce = _.debounce;
+        this.underscoreThrottle = _.throttle;
+        _.debounce = _.identity;
+        _.throttle = _.identity;
+
+        this.data = {
+            'mail.message': {
+                fields: {
+                    body: {
+                        string: "Contents",
+                        type: 'html',
+                    },
+                    author_id: {
+                        string: "Author",
+                        relation: 'res.partner',
+                    },
+                    channel_ids: {
+                        string: "Channels",
+                        type: 'many2many',
+                        relation: 'mail.channel',
+                    },
+                    starred: {
+                        string: "Starred",
+                        type: 'boolean',
+                    },
+                    needaction: {
+                        string: "Need Action",
+                        type: 'boolean',
+                    },
+                    starred_partner_ids: {
+                        string: "partner ids",
+                        type: 'integer',
+                    },
+                    model: {
+                        string: "Related Document model",
+                        type: 'char',
+                    },
+                    res_id: {
+                        string: "Related Document ID",
+                        type: 'integer',
+                    },
+                    need_moderation: {
+                        string: "Need moderation",
+                        type: 'boolean',
+                    },
+                    moderation_status: {
+                        string: "Moderation Status",
+                        type: 'integer',
+                        selection: [
+                            ['pending_moderation', 'Pending Moderation'],
+                            ['accepted', 'Accepted'],
+                            ['rejected', 'Rejected']
+                        ],
+                    },
+                },
+            },
+        };
+        this.services = [ChatManager, createBusService()];
+    },
+    afterEach: function () {
+        // unpatch _.debounce and _.throttle
+        _.debounce = this.underscoreDebounce;
+        _.throttle = this.underscoreThrottle;
+    }
+});
+
+QUnit.test('moderator: display moderation box', function (assert) {
+    assert.expect(1);
+    var done = assert.async();
+
+    this.data.initMessaging = {
+        channel_slots: {
+            channel_channel: [{
+                id: 1,
+                channel_type: "channel",
+                name: "general",
+                moderation: true,
+            }],
+        },
+        is_moderator: true,
+    };
+
+    createDiscuss({
+        id: 1,
+        context: {},
+        params: {},
+        data: this.data,
+        services: this.services,
+    })
+    .then(function (discuss) {
+        var moderationBoxSelector = '.o_mail_chat_channel_item' +
+                                    '[data-channel-id="channel_moderation"]';
+        assert.strictEqual(discuss.$(moderationBoxSelector).length, 1,
+            "there should be a moderation mailbox");
+        discuss.destroy();
+        done();
+    });
+});
+
+QUnit.test('moderator: moderated channel with pending moderation message', function (assert) {
+    assert.expect(33);
+    var done = assert.async();
+
+    this.data.initMessaging = {
+        channel_slots: {
+            channel_channel: [{
+                id: 1,
+                channel_type: "channel",
+                name: "general",
+                moderation: true,
+            }],
+        },
+        is_moderator: true,
+        moderation_counter: 1,
+        moderation_channel_ids: [1],
+    };
+    this.data['mail.message'].records = [{
+        author_id: [2, "Someone"],
+        body: "<p>test</p>",
+        id: 100,
+        model: 'mail.channel',
+        moderation_status: 'pending_moderation',
+        need_moderation: true,
+        res_id: 1,
+    }];
+
+    createDiscuss({
+        id: 1,
+        context: {},
+        params: {},
+        data: this.data,
+        services: this.services,
+    })
+    .then(function (discuss) {
+        var $moderationBox = discuss.$(
+                                '.o_mail_chat_channel_item' +
+                                '[data-channel-id="channel_moderation"]');
+        var $mailboxCounter = $moderationBox.find('.o_mail_sidebar_needaction.badge');
+        assert.strictEqual($mailboxCounter.length, 1,
+            "there should be a counter next to the moderation mailbox in the sidebar");
+        assert.strictEqual($mailboxCounter.text().trim(), "1",
+            "the mailbox counter of the moderation mailbox should display '1'");
+
+        // 1. go to moderation mailbox
+        $moderationBox.click();
+        var $message = discuss.$('.o_thread_message');
+        // check message
+        assert.strictEqual($message.length, 1,
+            "there should be one message in the moderation box");
+        assert.strictEqual($message.data('message-id'), 100,
+            "this message pending moderation should have correct ID");
+        assert.strictEqual($message.find('a[data-oe-id="1"]').text(), "#general",
+            "the message pending moderation should have correct origin as its linked document");
+        assert.strictEqual($message.find('.moderation_checkbox').length, 1,
+            "there should be a moderation checkbox next to the message");
+        assert.notOk($message.find('.moderation_checkbox').prop('checked'), false,
+            "the moderation checkbox should be unchecked by default");
+        // check moderation actions next to message
+        assert.strictEqual(discuss.$('.o_thread_message_moderation').length, 5,
+            "there should be 5 contextual moderation decisions next to the message");
+        assert.strictEqual(discuss.$('.o_thread_message_moderation[data-decision="accept"]').length, 1,
+            "there should be a contextual moderation decision to accept the message");
+        assert.strictEqual(discuss.$('.o_thread_message_moderation[data-decision="reject"]').length, 1,
+            "there should be a contextual moderation decision to reject the message");
+        assert.strictEqual(discuss.$('.o_thread_message_moderation[data-decision="discard"]').length, 1,
+            "there should be a contextual moderation decision to discard the message");
+        assert.strictEqual(discuss.$('.o_thread_message_moderation[data-decision="allow"]').length, 1,
+            "there should be a contextual moderation decision to allow the user of the message)");
+        assert.strictEqual(discuss.$('.o_thread_message_moderation[data-decision="ban"]').length, 1,
+            "there should be a contextual moderation decision to ban the user of the message");
+
+        // check select all (enabled) / unselect all (disabled) buttons
+        assert.strictEqual($('.o_mail_chat_button_select_all').length, 1,
+            "there should be a 'Select All' button in the control panel");
+        assert.notOk($('.o_mail_chat_button_select_all').hasClass('disabled'),
+            "the 'Select All' button should not be disabled");
+        assert.strictEqual($('.o_mail_chat_button_unselect_all').length, 1,
+            "there should be a 'Unselect All' button in the control panel");
+        assert.ok($('.o_mail_chat_button_unselect_all').hasClass('disabled'),
+            "the 'Unselect All' button should be disabled");
+        // check moderate all buttons (invisible)
+        var moderateAllSelector = '.o_mail_chat_button_moderate_all';
+        assert.strictEqual($(moderateAllSelector).length, 3,
+            "there should be 3 buttons to moderate selected messages in the control panel");
+        assert.strictEqual($(moderateAllSelector + '[data-decision="accept"]').length, 1,
+            "there should one moderate button to accept messages pending moderation");
+        assert.strictEqual($(moderateAllSelector + '[data-decision="accept"]').attr('style'),
+            'display: none;', 'the moderate button "Accept" should be invisible by default');
+        assert.strictEqual($(moderateAllSelector + '[data-decision="reject"]').length, 1,
+            "there should one moderate button to reject messages pending moderation");
+        assert.strictEqual($(moderateAllSelector + '[data-decision="reject"]').attr('style'),
+            'display: none;', 'the moderate button "Reject" should be invisible by default');
+        assert.strictEqual($(moderateAllSelector + '[data-decision="discard"]').length, 1,
+            "there should one moderate button to discard messages pending moderation");
+        assert.strictEqual($(moderateAllSelector + '[data-decision="discard"]').attr('style'),
+            'display: none;', 'the moderate button "Discard" should be invisible by default');
+
+        // click on message moderation checkbox
+        $message.find('.moderation_checkbox').click();
+        assert.ok($message.find('.moderation_checkbox').prop('checked'),
+            "the moderation checkbox should become checked after click");
+        // check select all (disabled) / unselect all buttons (enabled)
+        assert.ok($('.o_mail_chat_button_select_all').hasClass('disabled'),
+            "the 'Select All' button should be disabled");
+        assert.notOk($('.o_mail_chat_button_unselect_all').hasClass('disabled'),
+            "the 'Unselect All' button should not be disabled");
+        // check moderate all buttons updated (visible)
+        assert.strictEqual($(moderateAllSelector + '[data-decision="accept"]').attr('style'),
+            'display: inline-block;', 'the moderate button "Accept" should become visible');
+        assert.strictEqual($(moderateAllSelector + '[data-decision="reject"]').attr('style'),
+            'display: inline-block;', 'the moderate button "Reject" should become visible');
+        assert.strictEqual($(moderateAllSelector + '[data-decision="discard"]').attr('style'),
+            'display: inline-block;', 'the moderate button "Discard" should become visible');
+
+        // 2. go to channel 'general'
+        discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
+        $message = discuss.$('.o_thread_message');
+        // check correct message
+        assert.strictEqual($message.length, 1,
+            "there should be one message in the general channel");
+        assert.strictEqual($message.data('message-id'), 100,
+            "this message should have correct ID");
+        assert.notOk($message.find('.moderation_checkbox').prop('checked'),
+            "the moderation checkbox should be unchecked by default");
+
+        // don't check moderation actions visibility, since it is similar to moderation box.
+        discuss.destroy();
+        done();
+    });
+});
+
+QUnit.test('moderator: accept pending moderation message', function (assert) {
+    assert.expect(12);
+    var done = assert.async();
+
+    var bus = new Bus();
+    var BusService = createBusService(bus);
+
+    this.data.initMessaging = {
+        channel_slots: {
+            channel_channel: [{
+                id: 1,
+                channel_type: "channel",
+                name: "general",
+                moderation: true,
+            }],
+        },
+        is_moderator: true,
+        moderation_counter: 1,
+        moderation_channel_ids: [1],
+    };
+    this.data['mail.message'].records = [{
+        author_id: [2, "Someone"],
+        body: "<p>test</p>",
+        channel_ids: [],
+        id: 100,
+        model: 'mail.channel',
+        moderation_status: 'pending_moderation',
+        need_moderation: true,
+        res_id: 1,
+    }];
+
+    createDiscuss({
+        id: 1,
+        context: {},
+        params: {},
+        data: this.data,
+        services: [ChatManager, BusService],
+        mockRPC: function (route, args) {
+            if (args.method === 'moderate') {
+                assert.step('moderate');
+                var messageIDs = args.args[0];
+                var decision = args.args[1];
+                assert.strictEqual(messageIDs.length, 1, "should moderate one message");
+                assert.strictEqual(messageIDs[0], 100, "should moderate message with ID 100");
+                assert.strictEqual(decision, 'accept', "should accept the message");
+
+                // simulate notification back (new accepted message in channel)
+                var dbName = undefined; // useless for tests
+                var messageData = {
+                    author_id: [2, "Someone"],
+                    body: "<p>test</p>",
+                    channel_ids: [],
+                    id: 100,
+                    model: 'mail.channel',
+                    moderation_status: 'accepted'
+                };
+                var metaData = [dbName, 'mail.channel'];
+                var notification = [metaData, messageData];
+                bus.trigger('notification', [notification]);
+                return $.when();
+            }
+            return this._super.apply(this, arguments);
+        },
+    })
+    .then(function (discuss) {
+        // 1. go to moderation box
+        var $moderationBox = discuss.$(
+                                '.o_mail_chat_channel_item' +
+                                '[data-channel-id="channel_moderation"]');
+        $moderationBox.click();
+        // check there is a message to moderate
+        var $message = discuss.$('.o_thread_message');
+        assert.strictEqual($message.length, 1,
+            "there should be one message in the moderation box");
+        assert.strictEqual($message.data('message-id'), 100,
+            "this message should have correct ID");
+        assert.strictEqual($message.find('.moderation_checkbox').length, 1,
+            "the message should have a moderation checkbox");
+        // accept the message pending moderation
+        discuss.$('.o_thread_message_moderation[data-decision="accept"]').click();
+        assert.verifySteps(['moderate']);
+
+        // stop the fadeout animation and immediately remove the element
+        discuss.$('.o_thread_message').stop(false, true);
+        assert.strictEqual(discuss.$('.o_thread_message').length, 0,
+            "should now have no message displayed in moderation box");
+
+        // 2. go to channel 'general'
+        discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
+        $message = discuss.$('.o_thread_message');
+        // check message is there and has no moderate checkbox
+        assert.strictEqual($message.length, 1,
+            "there should be one message in the general channel");
+        assert.strictEqual($message.data('message-id'), 100,
+            "this message should have correct ID");
+        assert.strictEqual($message.find('.moderation_checkbox').length, 0,
+            "the message should not have any moderation checkbox");
+
+        discuss.destroy();
+        done();
+    });
+});
+
+QUnit.test('moderator: reject pending moderation message (reject with explanation)', function (assert) {
+    assert.expect(21);
+    var done = assert.async();
+
+    var bus = new Bus();
+    var BusService = createBusService(bus);
+
+    this.data.initMessaging = {
+        channel_slots: {
+            channel_channel: [{
+                id: 1,
+                channel_type: "channel",
+                name: "general",
+                moderation: true,
+            }],
+        },
+        is_moderator: true,
+        moderation_counter: 1,
+        moderation_channel_ids: [1],
+    };
+    this.data['mail.message'].records = [{
+        author_id: [2, "Someone"],
+        body: "<p>test</p>",
+        channel_ids: [],
+        id: 100,
+        model: 'mail.channel',
+        moderation_status: 'pending_moderation',
+        need_moderation: true,
+        res_id: 1,
+    }];
+
+    createDiscuss({
+        id: 1,
+        context: {},
+        params: {},
+        data: this.data,
+        services: [ChatManager, BusService],
+        mockRPC: function (route, args) {
+            if (args.method === 'moderate') {
+                assert.step('moderate');
+                var messageIDs = args.args[0];
+                var decision = args.args[1];
+                var kwargs = args.kwargs;
+                assert.strictEqual(messageIDs.length, 1, "should moderate one message");
+                assert.strictEqual(messageIDs[0], 100, "should moderate message with ID 100");
+                assert.strictEqual(decision, 'reject', "should reject the message");
+                assert.strictEqual(kwargs.title, "Message Rejected",
+                    "should have correct reject message title");
+                assert.strictEqual(kwargs.comment, "Your message was rejected by moderator.",
+                    "should have correct reject message body / comment");
+
+                // simulate notification back (deletion of rejected message in channel)
+                var dbName = undefined; // useless for tests
+                var notifData = {
+                    message_ids: messageIDs,
+                    type: "deletion",
+                };
+                var metaData = [dbName, 'res.partner'];
+                var notification = [metaData, notifData];
+                bus.trigger('notification', [notification]);
+                return $.when();
+            }
+            return this._super.apply(this, arguments);
+        },
+    })
+    .then(function (discuss) {
+        // 1. go to moderation box
+        var $moderationBox = discuss.$(
+                                '.o_mail_chat_channel_item' +
+                                '[data-channel-id="channel_moderation"]');
+        $moderationBox.click();
+        // check there is a message to moderate
+        var $message = discuss.$('.o_thread_message');
+        assert.strictEqual($message.length, 1,
+            "there should be one message in the moderation box");
+        assert.strictEqual($message.data('message-id'), 100,
+            "this message should have correct ID");
+        assert.strictEqual($message.find('.moderation_checkbox').length, 1,
+            "the message should have a moderation checkbox");
+        // reject the message pending moderation
+        discuss.$('.o_thread_message_moderation[data-decision="reject"]').click();
+
+        // check reject dialog prompt
+        assert.strictEqual($('.modal-dialog').length, 1,
+            "a dialog should be prompt to the moderator on click reject");
+        assert.strictEqual($('.modal-title').text(), "Send explanation to author",
+            "dialog should have correct title");
+        var $messageTitle = $('.modal-body input[id="message_title"]');
+        assert.strictEqual($messageTitle.length, 1,
+            "should have a title of message for rejecting the message");
+        assert.strictEqual($messageTitle.attr('placeholder'), "Subject",
+            "message title for reject reason should have correct placeholder");
+        assert.strictEqual($messageTitle.val(), "Message Rejected",
+            "message title for reject reason should have correct default value");
+        var $messageBody = $('.modal-body textarea[id="reject_message"]');
+        assert.strictEqual($messageBody.length, 1,
+            "should have a body of message for rejecting the message");
+        assert.strictEqual($messageBody.attr('placeholder'), "Mail Body",
+            "message body for reject reason should have correct placeholder");
+        assert.strictEqual($messageBody.text(), "Your message was rejected by moderator.",
+            "message body for reject reason should have correct default text content");
+        assert.strictEqual($('.modal-footer button').text(), "Send",
+            "should have a send button on the reject dialog");
+
+        // send mesage
+        $('.modal-footer button').click();
+        assert.verifySteps(['moderate']);
+
+        // stop the fadeout animation and immediately remove the element
+        discuss.$('.o_thread_message').stop(false, true);
+        assert.strictEqual(discuss.$('.o_thread_message').length, 0,
+            "should now have no message displayed in moderation box");
+
+        // 2. go to channel 'general'
+        discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
+        assert.strictEqual(discuss.$('.o_thread_message').length, 0,
+            "should now have no message displayed in moderated channel");
+
+        discuss.destroy();
+        done();
+    });
+});
+
+QUnit.test('moderator: discard pending moderation message (reject without explanation)', function (assert) {
+    assert.expect(15);
+    var done = assert.async();
+
+    var bus = new Bus();
+    var BusService = createBusService(bus);
+
+    this.data.initMessaging = {
+        channel_slots: {
+            channel_channel: [{
+                id: 1,
+                channel_type: "channel",
+                name: "general",
+                moderation: true,
+            }],
+        },
+        is_moderator: true,
+        moderation_counter: 1,
+        moderation_channel_ids: [1],
+    };
+    this.data['mail.message'].records = [{
+        author_id: [2, "Someone"],
+        body: "<p>test</p>",
+        channel_ids: [],
+        id: 100,
+        model: 'mail.channel',
+        moderation_status: 'pending_moderation',
+        need_moderation: true,
+        res_id: 1,
+    }];
+
+    createDiscuss({
+        id: 1,
+        context: {},
+        params: {},
+        data: this.data,
+        services: [ChatManager, BusService],
+        mockRPC: function (route, args) {
+            if (args.method === 'moderate') {
+                assert.step('moderate');
+                var messageIDs = args.args[0];
+                var decision = args.args[1];
+                assert.strictEqual(messageIDs.length, 1, "should moderate one message");
+                assert.strictEqual(messageIDs[0], 100, "should moderate message with ID 100");
+                assert.strictEqual(decision, 'discard', "should discard the message");
+
+                // simulate notification back (deletion of rejected message in channel)
+                var dbName = undefined; // useless for tests
+                var notifData = {
+                    message_ids: messageIDs,
+                    type: 'deletion',
+                };
+                var metaData = [dbName, 'res.partner'];
+                var notification = [metaData, notifData];
+                bus.trigger('notification', [notification]);
+                return $.when();
+            }
+            return this._super.apply(this, arguments);
+        },
+    })
+    .then(function (discuss) {
+        // 1. go to moderation box
+        var $moderationBox = discuss.$(
+                                '.o_mail_chat_channel_item' +
+                                '[data-channel-id="channel_moderation"]');
+        $moderationBox.click();
+        // check there is a message to moderate
+        var $message = discuss.$('.o_thread_message');
+        assert.strictEqual($message.length, 1,
+            "there should be one message in the moderation box");
+        assert.strictEqual($message.data('message-id'), 100,
+            "this message should have correct ID");
+        assert.strictEqual($message.find('.moderation_checkbox').length, 1,
+            "the message should have a moderation checkbox");
+        // discard the message pending moderation
+        discuss.$('.o_thread_message_moderation[data-decision="discard"]').click();
+
+        // check discard dialog prompt
+        assert.strictEqual($('.modal-dialog').length, 1,
+            "a dialog should be prompt to the moderator on click discard");
+        assert.strictEqual($('.modal-body').text(),
+            "You are going to discard 1 message. Do you confirm the action?",
+            "should warn the user on discard action");
+        assert.strictEqual($('.modal-footer button').length, 2,
+            "should have two buttons in the footer of the dialog");
+        assert.strictEqual($('.modal-footer button.btn-primary').text(), "Ok",
+            "should have a confirm button in the dialog for discard");
+        assert.strictEqual($('.modal-footer button.btn-default').text(), "Cancel",
+            "should have a cancel button in the dialog for discard");
+
+        // discard mesage
+        $('.modal-footer button.btn-primary').click();
+        assert.verifySteps(['moderate']);
+
+        // stop the fadeout animation and immediately remove the element
+        discuss.$('.o_thread_message').stop(false, true);
+        assert.strictEqual(discuss.$('.o_thread_message').length, 0,
+            "should now have no message displayed in moderation box");
+
+        // 2. go to channel 'general'
+        discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
+        assert.strictEqual(discuss.$('.o_thread_message').length, 0,
+            "should now have no message displayed in moderated channel");
+
+        discuss.destroy();
+        done();
+    });
+});
+
+QUnit.test('author: send message in moderated channel', function (assert) {
+    assert.expect(4);
+    var done = assert.async();
+
+    var bus = new Bus();
+    var BusService = createBusService(bus);
+
+    var messagePostDef = $.Deferred();
+
+    this.data.initMessaging = {
+        channel_slots: {
+            channel_channel: [{
+                id: 1,
+                channel_type: "channel",
+                name: "general",
+                moderation: true,
+            }],
+        },
+    };
+
+    createDiscuss({
+        id: 1,
+        context: {},
+        params: {},
+        data: this.data,
+        services: [ChatManager, BusService],
+        mockRPC: function (route, args) {
+            if (args.method === 'message_post') {
+                var message = {
+                    id: 100,
+                    author_id: [2, 'Someone'],
+                    body: args.kwargs.body,
+                    message_type: args.kwargs.message_type,
+                    model: 'mail.channel',
+                    moderation_status: 'pending_moderation',
+                    res_id: 1,
+                };
+                var metaData = [undefined, 'res.partner'];
+                var notifData = {
+                    type: 'author',
+                    message: message,
+                };
+                var notification = [metaData, notifData];
+                bus.trigger('notification', [notification]);
+
+                messagePostDef.resolve();
+                return $.when(message.id);
+            }
+            return this._super.apply(this, arguments);
+        },
+        session: {
+            partner_id: 2,
+        },
+    })
+    .then(function (discuss) {
+
+        // go to channel 'general'
+        discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
+        // post a message
+        discuss.$('.o_composer_input textarea').first().val("some text");
+        discuss.$('.o_composer_send button').click();
+
+        messagePostDef
+            .then(function () {
+
+                var $message = discuss.$('.o_thread_message');
+                assert.strictEqual($message.length, 1,
+                    "should have a message in the thread");
+                assert.strictEqual($message.data('message-id'), 100,
+                    "message should have ID returned from 'message_post'");
+                assert.strictEqual($message.find('.o_thread_author').text().trim(),
+                    "Someone", "message should have correct author displayed name");
+                assert.strictEqual(discuss.$('.o_thread_icons i.text-danger').text(),
+                    "Pending moderation", "the message should be pending moderation");
+
+                discuss.destroy();
+                done();
+            });
+    });
+});
+
+QUnit.test('author: sent message accepted in moderated channel', function (assert) {
+    assert.expect(8);
+    var done = assert.async();
+
+    var bus = new Bus();
+    var BusService = createBusService(bus);
+
+    this.data.initMessaging = {
+        channel_slots: {
+            channel_channel: [{
+                id: 1,
+                channel_type: "channel",
+                name: "general",
+                moderation: true,
+            }],
+        },
+    };
+
+    this.data['mail.message'].records = [{
+        author_id: [2, "Someone"],
+        body: "<p>test</p>",
+        channel_ids: [],
+        id: 100,
+        model: 'mail.channel',
+        moderation_status: 'pending_moderation',
+        need_moderation: true,
+        res_id: 1,
+    }];
+
+    createDiscuss({
+        id: 1,
+        context: {},
+        params: {},
+        data: this.data,
+        services: [ChatManager, BusService],
+        session: {
+            partner_id: 2,
+        },
+    })
+    .then(function (discuss) {
+
+        // go to channel 'general'
+        discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
+        // check message is pending
+        var $message = discuss.$('.o_thread_message');
+        assert.strictEqual($message.length, 1,
+            "should have a message in the thread");
+        assert.strictEqual($message.data('message-id'), 100,
+            "message should have ID returned from 'message_post'");
+        assert.strictEqual($message.find('.o_thread_author').text().trim(),
+            "Someone", "message should have correct author displayed name");
+        assert.strictEqual(discuss.$('.o_thread_icons i.text-danger').text(),
+            "Pending moderation", "the message should be pending moderation");
+
+        // simulate accepted message
+        var dbName = undefined; // useless for tests
+        var messageData = {
+            author_id: [2, "Someone"],
+            body: "<p>test</p>",
+            channel_ids: [],
+            id: 100,
+            model: 'mail.channel',
+            moderation_status: 'accepted'
+        };
+        var metaData = [dbName, 'mail.channel'];
+        var notification = [metaData, messageData];
+        bus.trigger('notification', [notification]);
+
+        // // check message is accepted
+        $message = discuss.$('.o_thread_message');
+        assert.strictEqual($message.length, 1,
+            "should still have a message in the thread");
+        assert.strictEqual($message.data('message-id'), 100,
+            "message should still have ID returned from 'message_post'");
+        assert.strictEqual($message.find('.o_thread_author').text().trim(),
+            "Someone", "message should still have correct author displayed name");
+        assert.strictEqual(discuss.$('.o_thread_icons i.text-danger').length, 0,
+            "the message should not be in pending moderation anymore");
+
+        discuss.destroy();
+        done();
+    });
+});
+
+QUnit.test('author: sent message rejected in moderated channel', function (assert) {
+    assert.expect(5);
+    var done = assert.async();
+
+    var bus = new Bus();
+    var BusService = createBusService(bus);
+
+    this.data.initMessaging = {
+        channel_slots: {
+            channel_channel: [{
+                id: 1,
+                channel_type: "channel",
+                name: "general",
+                moderation: true,
+            }],
+        },
+    };
+
+    this.data['mail.message'].records = [{
+        author_id: [2, "Someone"],
+        body: "<p>test</p>",
+        channel_ids: [],
+        id: 100,
+        model: 'mail.channel',
+        moderation_status: 'pending_moderation',
+        need_moderation: true,
+        res_id: 1,
+    }];
+
+    createDiscuss({
+        id: 1,
+        context: {},
+        params: {},
+        data: this.data,
+        services: [ChatManager, BusService],
+        session: {
+            partner_id: 2,
+        },
+    })
+    .then(function (discuss) {
+
+        // go to channel 'general'
+        discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
+        // check message is pending
+        var $message = discuss.$('.o_thread_message');
+        assert.strictEqual($message.length, 1,
+            "should have a message in the thread");
+        assert.strictEqual($message.data('message-id'), 100,
+            "message should have ID returned from 'message_post'");
+        assert.strictEqual($message.find('.o_thread_author').text().trim(),
+            "Someone", "message should have correct author displayed name");
+        assert.strictEqual(discuss.$('.o_thread_icons i.text-danger').text(),
+            "Pending moderation", "the message should be pending moderation");
+
+        // simulate reject from moderator
+        var dbName = undefined; // useless for tests
+        var notifData = {
+            type: 'deletion',
+            message_ids: [100],
+        };
+        var metaData = [dbName, 'res.partner'];
+        var notification = [metaData, notifData];
+        bus.trigger('notification', [notification]);
+
+        // // check no message
+        assert.strictEqual(discuss.$('.o_thread_message').length, 0,
+            "message should be removed from channel after reject");
+
+        discuss.destroy();
+        done();
+    });
+});
+
+});
+});
diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js
index 8b7c72fd21e8..a56a7c64855e 100644
--- a/addons/mail/static/tests/discuss_tests.js
+++ b/addons/mail/static/tests/discuss_tests.js
@@ -44,13 +44,21 @@ QUnit.module('Discuss client action', {
                         type: 'boolean',
                     },
                     needaction: {
-                      string: "Need Action",
-                      type: 'boolean',
-                  },
-                  starred_partner_ids: {
-                      string: "partner ids",
-                      type: 'integer',
-                  }
+                        string: "Need Action",
+                        type: 'boolean',
+                    },
+                    starred_partner_ids: {
+                        string: "partner ids",
+                        type: 'integer',
+                    },
+                    model: {
+                        string: "Related Document model",
+                        type: 'char',
+                    },
+                    res_id: {
+                        string: "Related Document ID",
+                        type: 'integer',
+                    },
                 },
             },
         };
diff --git a/addons/mail/views/mail_channel_views.xml b/addons/mail/views/mail_channel_views.xml
index d5acaae761ea..24115dde2fb7 100644
--- a/addons/mail/views/mail_channel_views.xml
+++ b/addons/mail/views/mail_channel_views.xml
@@ -83,7 +83,16 @@
             <field name="priority" eval="10"/>
             <field name="arch" type="xml">
                 <form string="Mail Channel Form">
+                    <header>
+                        <button name="send_guidelines" type="object" string="Send guidelines" confirm="You are going to send the guidelines to all the subscribers. Do you confirm the action?" attrs="{'invisible':['|',('moderation_guidelines','=',False), ('is_moderator', '=', False)]}"/>
+                    </header>
                     <sheet>
+                        <div class="oe_button_box" name="button_box">
+                            <field name="is_moderator" invisible="1"/>
+                            <button class="btn btn-sm oe_stat_button" name="mail.mail_moderation_action" type="action" attrs="{'invisible':[('is_moderator', '=', False)]}" icon="fa-bars" context="{'search_default_channel_id': active_id}">
+                            <field string="Ban List" name="moderation_count" widget="statinfo"/>
+                            </button>
+                        </div>
                         <div class="oe_button_box" name="button_box"/>
                         <field name="image" widget="image" class="oe_avatar" options="{'preview_image': 'image_medium', 'size': [90, 90]}"/>
                         <div class="oe_title">
@@ -96,6 +105,7 @@
                         </div>
                         <group class="o_label_nowrap">
                             <field name="email_send"/>
+                            <field name="moderation" attrs="{'invisible': [('email_send', '=', False)]}"/>
                             <field name="description" placeholder="Topics discussed in this group..."/>
                         </group>
                         <group name="group_alias" attrs="{'invisible': [('alias_domain', '=', False)]}">
@@ -123,10 +133,19 @@
                                 <field name="channel_last_seen_partner_ids" mode="tree" context="{'active_test': False}">
                                     <tree string="Members" editable="bottom">
                                         <field name="partner_id"/>
-                                        <field name="partner_email"/>
+                                        <field name="partner_email" readonly="1"/>
                                     </tree>
                                 </field>
                             </page>
+                            <page string="Moderation" attrs="{'invisible': [('moderation', '=', False)]}">
+                                <group>
+                                    <field name="moderator_ids" widget="many2many_tags"/>
+                                    <field name="moderation_notify"/>
+                                    <field name="moderation_notify_msg" attrs="{'invisible': [('moderation_notify', '=', False)]}"/>
+                                    <field name="moderation_guidelines"/>
+                                    <field name="moderation_guidelines_msg" attrs="{'invisible':[('moderation_guidelines', '=', False)]}"/>
+                                </group>
+                            </page>
                             <page string="Integrations" invisible="0" name="mail_channel_integrations"></page>
                         </notebook>
                         <div class="oe_chatter" groups="base.group_no_one">
@@ -136,7 +155,7 @@
                 </form>
             </field>
         </record>
-
+        
         <record id="mail_channel_view_tree" model="ir.ui.view">
             <field name="name">mail.channel.tree</field>
             <field name="model">mail.channel</field>
diff --git a/addons/mail/views/mail_message_views.xml b/addons/mail/views/mail_message_views.xml
index 5e2fb5514a2e..09bb34c86ac3 100644
--- a/addons/mail/views/mail_message_views.xml
+++ b/addons/mail/views/mail_message_views.xml
@@ -32,6 +32,7 @@
                                 <field name="email_from"/>
                                 <field name="author_id"/>
                                 <field name="record_name"/>
+                                <field name="moderator_id"/>
                             </group>
                             <group>
                                 <field name="parent_id"/>
@@ -39,6 +40,7 @@
                                 <field name="res_id"/>
                                 <field name="message_type"/>
                                 <field name="subtype_id"/>
+                                <field name="moderation_status"/>
                             </group>
                         </group>
                         <notebook>
diff --git a/addons/mail/views/mail_moderation_views.xml b/addons/mail/views/mail_moderation_views.xml
new file mode 100644
index 000000000000..41b00dc1c3b3
--- /dev/null
+++ b/addons/mail/views/mail_moderation_views.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<odoo>
+    <data>
+        <record id="mail_moderation_view_tree" model="ir.ui.view">
+            <field name="name">mail.moderation.view.tree</field>
+            <field name="model">mail.moderation</field>
+            <field name="priority">20</field>
+            <field name="arch" type="xml">
+                <tree string="Moderation Lists" editable="bottom">
+                    <field name="channel_id"/>
+                    <field name="email"/>
+                    <field name="status"/>
+                </tree>
+            </field>
+        </record>
+
+        <record id="mail_moderation_view_search" model="ir.ui.view">
+            <field name="name">mail.moderation.view.search</field>
+            <field name="model">mail.moderation</field>
+            <field name="priority">25</field>
+            <field name="arch" type="xml">
+                <search string="Search Moderation List">
+                    <field name="channel_id"/>
+                    <field name="email"/>
+                    <field name="status"/>
+                    <filter string="Is Banned"
+                            name="status_ban" help="Banned Emails"
+                            domain="[('status', '=', 'ban')]"/>
+                    <separator/>
+                    <filter string="Is Allowed"
+                            name="status_allow" help="Allowed Emails"
+                            domain="[('status', '=', 'allow')]"/>
+                </search>
+            </field>
+        </record>
+
+        <record id="mail_moderation_action" model="ir.actions.act_window">
+            <field name="name">Moderation</field>
+            <field name="res_model">mail.moderation</field>
+            <field name="view_type">form</field>
+            <field name="view_mode">tree,form</field>
+            <field name="search_view_id" ref="mail_moderation_view_search"/>
+            <field name="context">{'search_default_status_ban': 1}</field>
+        </record>
+
+        <!-- Add menu entry in Settings/Email -->
+        <menuitem name="White List / Black List"
+            id="mail_moderation_menu"
+            parent="base.menu_email"
+            action="mail_moderation_action"/>
+    </data>
+</odoo>
diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml
index af2bc8b3b75b..5dd1b057bd9c 100644
--- a/addons/mail/views/mail_templates.xml
+++ b/addons/mail/views/mail_templates.xml
@@ -53,6 +53,7 @@
                 <script type="text/javascript" src="/mail/static/tests/mail_utils_tests.js"></script>
                 <script type="text/javascript" src="/mail/static/tests/discuss_tests.js"></script>
                 <script type="text/javascript" src="/mail/static/tests/systray_tests.js"></script>
+                <script type="text/javascript" src="/mail/static/tests/discuss_moderation_tests.js"></script>  
                 <script type="text/javascript" src="/mail/static/tests/helpers/mock_server.js"></script>
                 <script type="text/javascript" src="/mail/static/tests/helpers/test_utils.js"></script>
             </xpath>
diff --git a/addons/test_mail/tests/__init__.py b/addons/test_mail/tests/__init__.py
index dc9978773c42..411d73d5694a 100644
--- a/addons/test_mail/tests/__init__.py
+++ b/addons/test_mail/tests/__init__.py
@@ -13,4 +13,5 @@ from . import test_invite
 from . import test_ir_actions
 from . import test_update_notification
 from . import test_discuss
-from . import test_performance
\ No newline at end of file
+from . import test_performance
+from . import test_res_users
\ No newline at end of file
diff --git a/addons/test_mail/tests/common.py b/addons/test_mail/tests/common.py
index e536e15fa708..b526dce2906f 100644
--- a/addons/test_mail/tests/common.py
+++ b/addons/test_mail/tests/common.py
@@ -1,4 +1,5 @@
 # -*- coding: utf-8 -*-
+from json import dumps, loads
 
 import json
 
@@ -7,7 +8,7 @@ from email.utils import formataddr
 
 from odoo import api
 from odoo.addons.bus.models.bus import json_dump
-from odoo.tests import common
+from odoo.tests import common, tagged
 
 
 class BaseFunctionalTest(common.SavepointCase):
@@ -244,6 +245,7 @@ class MockEmails(common.SingleTransactionCase):
         super(MockEmails, cls).tearDownClass()
 
     def _init_mock_build_email(self):
+        self.env['mail.mail'].search([]).unlink()
         self._mails_args[:] = []
         self._mails[:] = []
 
@@ -260,3 +262,57 @@ class MockEmails(common.SingleTransactionCase):
         mail = self.format(template, to=to, subject=subject, cc=cc, extra=extra, email_from=email_from, msg_id=msg_id)
         self.env['mail.thread'].with_context(mail_channel_noautofollow=True).message_process(model, mail)
         return self.env[target_model].search([(target_field, '=', subject)])
+
+
+@tagged('moderation')
+class Moderation(MockEmails, BaseFunctionalTest):
+
+    @classmethod
+    def setUpClass(cls):
+        super(Moderation, cls).setUpClass()
+        Channel = cls.env['mail.channel']
+        Users = cls.env['res.users'].with_context(cls._quick_create_user_ctx)
+
+        cls.channel_moderation_1 = Channel.create({
+            'name': 'Moderation_1',
+            'email_send': True,
+            'moderation': True
+            })
+        cls.channel_1 = cls.channel_moderation_1
+        cls.channel_moderation_2 = Channel.create({
+            'name': 'Moderation_2',
+            'email_send': True,
+            'moderation': True
+            })
+        cls.channel_2 = cls.channel_moderation_2
+
+        cls.user_employee.write({'moderation_channel_ids': [(6, 0, [cls.channel_1.id])]})
+
+        cls.user_employee_2 = Users.create({
+            'name': 'Roboute',
+            'login': 'roboute',
+            'email': 'roboute@guilliman.com',
+            'groups_id': [(6, 0, [cls.env.ref('base.group_user').id])],
+            'moderation_channel_ids': [(6, 0, [cls.channel_2.id])]
+            })
+        cls.partner_employee_2 = cls.user_employee_2.partner_id
+
+        cls.channel_moderation_1.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': cls.partner_employee.id})]})
+        cls.channel_moderation_2.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': cls.partner_employee_2.id})]})
+
+    def _create_new_message(self, channel_id, status='pending_moderation', author=None, body='', message_type="email"):
+        author = author if author else self.env.user.partner_id
+        message = self.env['mail.message'].create({
+            'model': 'mail.channel',
+            'res_id': channel_id,
+            'message_type': 'email',
+            'body': body,
+            'moderation_status': status,
+            'author_id': author.id,
+            'email_from': formataddr((author.name, author.email)),
+            'subtype_id': self.env['mail.message.subtype'].search([('name', '=', 'Discussions')]).id
+            })
+        return message
+
+    def _clear_bus(self):
+        self.env['bus.bus'].search([]).unlink()
diff --git a/addons/test_mail/tests/test_mail_channel.py b/addons/test_mail/tests/test_mail_channel.py
index 73fa55ebbcc4..395fbd7b3185 100644
--- a/addons/test_mail/tests/test_mail_channel.py
+++ b/addons/test_mail/tests/test_mail_channel.py
@@ -1,9 +1,9 @@
 # -*- coding: utf-8 -*-
-
 from email.utils import formataddr
 
+from odoo.tests import tagged
 from odoo.addons.test_mail.tests import common
-from odoo.exceptions import AccessError, except_orm
+from odoo.exceptions import AccessError, except_orm, ValidationError, UserError
 from odoo.tools import mute_logger
 
 
@@ -195,3 +195,103 @@ class TestChannelFeatures(common.BaseFunctionalTest, common.MockEmails):
             self.assertIn(
                 email['email_to'][0],
                 [formataddr((self.user_employee.name, self.user_employee.email)), formataddr((self.test_partner.name, self.test_partner.email))])
+
+
+@tagged('moderation')
+class TestChannelModeration(common.Moderation):
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestChannelModeration, cls).setUpClass()
+
+    def test_moderator_consistency(self):
+        with self.assertRaises(ValidationError):
+            self.channel_1.write({'moderator_ids': [(4, self.user_employee_2.id)]})
+
+        self.channel_1.write({'channel_partner_ids': [(4, self.partner_employee_2.id)]})
+        with self.assertRaises(ValidationError):
+            self.user_employee_2.write({'email': False})
+            self.channel_1.write({'moderator_ids': [(4, self.user_employee_2.id)]})
+
+    def test_channel_moderation_parameters(self):
+        with self.assertRaises(ValidationError):
+            self.channel_1.write({'email_send': False})
+
+        with self.assertRaises(ValidationError):
+            self.channel_1.write({'moderator_ids': [(5, 0)]})
+
+    def test_moderation_count(self):
+        self.assertEqual(self.channel_1.moderation_count, 0)
+        self.channel_1.write({'moderation_ids': [
+            (0, 0, {'email': 'test0@example.com', 'status': 'allow'}),
+            (0, 0, {'email': 'test1@example.com', 'status': 'ban'})
+        ]})
+        self.assertEqual(self.channel_1.moderation_count, 2)
+
+    @mute_logger('odoo.addons.mail.models.mail_channel', 'odoo.models.unlink')
+    def test_send_guidelines(self):
+        self.channel_1.write({'channel_partner_ids': [(4, self.partner_employee_2.id), (4, self.partner_admin.id)]})
+        self.channel_1._update_moderation_email([self.partner_admin.email], 'ban')
+        self._init_mock_build_email()
+        self.channel_1.sudo(self.user_employee).send_guidelines()
+        self.env['mail.mail'].process_email_queue()
+        self.assertEmails(False, self.partner_employee | self.partner_employee_2, email_from=self.env.user.company_id.catchall or self.env.user.company_id.email)
+
+    def test_send_guidelines_crash(self):
+        with self.assertRaises(UserError):
+            self.channel_1.sudo(self.user_employee_2).send_guidelines()
+
+    def test_update_moderation_email(self):
+        self.channel_1.write({'moderation_ids': [
+            (0, 0, {'email': 'test0@example.com', 'status': 'allow'}),
+            (0, 0, {'email': 'test1@example.com', 'status': 'ban'})
+        ]})
+        self.channel_1._update_moderation_email(['test0@example.com', 'test3@example.com'], 'ban')
+        self.assertEqual(len(self.channel_1.moderation_ids), 3)
+        self.assertTrue(all(status == 'ban' for status in self.channel_1.moderation_ids.mapped('status')))
+
+    def test_moderation_reset(self):
+        self._create_new_message(self.channel_1.id)
+        self._create_new_message(self.channel_1.id, status='accepted')
+        self._create_new_message(self.channel_2.id)
+        self.channel_1.write({'moderation': False})
+        self.assertEqual(self.env['mail.message'].search_count([
+            ('moderation_status', '=', 'pending_moderation'),
+            ('model', '=', 'mail.channel'),
+            ('res_id', '=', self.channel_1.id)
+        ]), 0)
+        self.assertEqual(self.env['mail.message'].search_count([
+            ('moderation_status', '=', 'pending_moderation'),
+            ('model', '=', 'mail.channel'),
+            ('res_id', '=', self.channel_2.id)
+        ]), 1)
+        self.channel_2.write({'moderation': False})
+        self.assertEqual(self.env['mail.message'].search_count([
+            ('moderation_status', '=', 'pending_moderation'),
+            ('model', '=', 'mail.channel'),
+            ('res_id', '=', self.channel_2.id)
+        ]), 0)
+
+    @mute_logger('odoo.models.unlink')
+    def test_message_post(self):
+        email1 = 'test0@example.com'
+        email2 = 'test1@example.com'
+
+        self.channel_1._update_moderation_email([email1], 'ban')
+        self.channel_1._update_moderation_email([email2], 'allow')
+
+        msg_admin = self.channel_1.message_post(message_type='email', subtype='mt_comment', author_id=self.partner_admin.id)
+        msg_moderator = self.channel_1.message_post(message_type='comment', subtype='mt_comment', author_id=self.partner_employee.id)
+        msg_email1 = self.channel_1.message_post(message_type='comment', subtype='mt_comment', email_from=formataddr(("MyName", email1)))
+        msg_email2 = self.channel_1.message_post(message_type='email', subtype='mt_comment', email_from=email2)
+        msg_notif = self.channel_1.message_post()
+
+        messages = self.env['mail.message'].search([('model', '=', 'mail.channel'), ('res_id', '=', self.channel_1.id)])
+        pending_messages = messages.filtered(lambda m: m.moderation_status == 'pending_moderation')
+        accepted_messages = messages.filtered(lambda m: m.moderation_status == 'accepted')
+
+        self.assertFalse(msg_email1)
+        self.assertEqual(msg_admin, pending_messages)
+        self.assertEqual(accepted_messages, msg_moderator | msg_email2 | msg_notif)
+        self.assertFalse(msg_admin.channel_ids)
+        self.assertEqual(msg_email2.channel_ids, self.channel_1)
diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py
index 5d747e0a207f..94540272b4e2 100644
--- a/addons/test_mail/tests/test_mail_message.py
+++ b/addons/test_mail/tests/test_mail_message.py
@@ -5,6 +5,7 @@ import base64
 from odoo.addons.test_mail.tests import common
 from odoo.exceptions import AccessError, except_orm
 from odoo.tools import mute_logger
+from odoo.tests import tagged
 
 
 class TestMessageValues(common.BaseFunctionalTest, common.MockEmails):
@@ -213,6 +214,10 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
         messages = self.env['mail.message'].sudo(self.user_portal).search([('subject', 'like', '_ZTest')])
         self.assertEqual(messages, msg4 | msg5)
 
+    # --------------------------------------------------
+    # READ
+    # --------------------------------------------------
+
     @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
     def test_mail_message_access_read_crash(self):
         # TODO: Change the except_orm to Warning ( Because here it's call check_access_rule
@@ -251,6 +256,16 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
         # Test: Bert reads the message, ok because linked to a doc he is allowed to read
         self.message.sudo(self.user_employee).read()
 
+    def test_mail_message_access_read_crash_moderation(self):
+        # with self.assertRaises(AccessError):
+        self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'})
+        # Test: Bert reads the message, ok because linked to a doc he is allowed to read
+        self.message.sudo(self.user_employee).read()
+
+    # --------------------------------------------------
+    # CREATE
+    # --------------------------------------------------
+
     @mute_logger('odoo.addons.base.models.ir_model')
     def test_mail_message_access_create_crash_public(self):
         # Do: Bert creates a message on Pigs -> ko, no creation rights
@@ -284,6 +299,25 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
         self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
         self.env['mail.message'].sudo(self.user_employee).create({'model': 'mail.channel', 'res_id': self.group_private.id, 'body': 'Test', 'parent_id': self.message.id})
 
+    # --------------------------------------------------
+    # WRITE
+    # --------------------------------------------------
+
+    def test_mail_message_access_write_moderation(self):
+        """ Only moderators can modify pending messages """
+        self.group_public.write({
+            'email_send': True,
+            'moderation': True,
+            'channel_partner_ids': [(4, self.partner_employee.id)],
+            'moderator_ids': [(4, self.user_employee.id)],
+        })
+        self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'})
+        self.message.sudo(self.user_employee).write({'moderation_status': 'accepted'})
+
+    def test_mail_message_access_write_crash_moderation(self):
+        self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'})
+        with self.assertRaises(AccessError):
+            self.message.sudo(self.user_employee).write({'moderation_status': 'accepted'})
 
     @mute_logger('openerp.addons.mail.models.mail_mail')
     def test_mark_all_as_read(self):
@@ -350,4 +384,73 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
 
         portal_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
         na_count = portal_partner.get_needaction_count()
-        self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
\ No newline at end of file
+        self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
+
+
+@tagged('moderation')
+class TestMessageModeration(common.Moderation):
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestMessageModeration, cls).setUpClass()
+
+        cls.msg_admin_pending_c1 = cls._create_new_message(cls, cls.channel_1.id, status='pending_moderation', author=cls.partner_admin)
+        cls.msg_admin_pending_c1_2 = cls._create_new_message(cls, cls.channel_1.id, status='pending_moderation', author=cls.partner_admin)
+        cls.msg_emp2_pending_c1 = cls._create_new_message(cls, cls.channel_1.id, status='pending_moderation', author=cls.partner_employee_2)
+
+    @mute_logger('odoo.models.unlink')
+    def test_moderate_accept(self):
+        self._clear_bus()
+        # A pending moderation message needs to have field channel_ids empty. Moderators
+        # need to be able to notify a pending moderation message (in a channel they moderate).
+        self.assertFalse(self.msg_admin_pending_c1.channel_ids)
+        self.msg_admin_pending_c1.sudo(self.user_employee)._moderate('accept')
+        self.assertEqual(self.msg_admin_pending_c1.channel_ids, self.channel_1)
+        self.assertEqual(self.msg_admin_pending_c1.moderation_status, 'accepted')
+        self.assertEqual(self.msg_admin_pending_c1_2.moderation_status, 'pending_moderation')
+        self.assertBusNotification([(self.cr.dbname, 'mail.channel', self.channel_1.id)])
+
+    @mute_logger('odoo.models.unlink')
+    def test_moderate_allow(self):
+        self._clear_bus()
+        # A pending moderation message needs to have field channel_ids empty. Moderators
+        # need to be able to notify a pending moderation message (in a channel they moderate).
+        self.assertFalse(self.msg_admin_pending_c1.channel_ids)
+        self.assertFalse(self.msg_admin_pending_c1_2.channel_ids)
+        self.msg_admin_pending_c1.sudo(self.user_employee)._moderate('allow')
+        self.assertEqual(self.msg_admin_pending_c1.channel_ids, self.channel_1)
+        self.assertEqual(self.msg_admin_pending_c1_2.channel_ids, self.channel_1)
+        self.assertEqual(self.msg_admin_pending_c1.moderation_status, 'accepted')
+        self.assertEqual(self.msg_admin_pending_c1_2.moderation_status, 'accepted')
+        self.assertBusNotification([
+            (self.cr.dbname, 'mail.channel', self.channel_1.id),
+            (self.cr.dbname, 'mail.channel', self.channel_1.id)])
+
+    @mute_logger('odoo.models.unlink')
+    def test_moderate_reject(self):
+        self._init_mock_build_email()
+        (self.msg_admin_pending_c1 | self.msg_emp2_pending_c1).sudo(self.user_employee)._moderate_send_reject_email('Title', 'Message to author')
+        self.env['mail.mail'].process_email_queue()
+        self.assertEmails(self.partner_employee, self.partner_employee_2 | self.partner_admin, subject='Title', body_content='Message to author')
+
+    def test_moderate_discard(self):
+        self._clear_bus()
+        id1, id2 = self.msg_admin_pending_c1.id, self.msg_emp2_pending_c1.id  # save ids because unlink will discard them
+        (self.msg_admin_pending_c1 | self.msg_emp2_pending_c1).sudo(self.user_employee)._moderate_discard()
+
+        self.assertBusNotification(
+            [(self.cr.dbname, 'res.partner', self.partner_admin.id),
+             (self.cr.dbname, 'res.partner', self.partner_employee_2.id),
+             (self.cr.dbname, 'res.partner', self.partner_employee.id)],
+            [{'type': 'deletion', 'message_ids': [id1]},  # admin: one message deleted because discarded
+             {'type': 'deletion', 'message_ids': [id2]},  # employee_2: one message delete because discarded
+             {'type': 'deletion', 'message_ids': [id1, id2]}]  # employee: two messages deleted because moderation done
+        )
+
+    @mute_logger('odoo.models.unlink')
+    def test_notify_moderators(self):
+        # create pending messages in another channel to have two notification to push
+        msg_emp_pending_c2 = self._create_new_message(self.channel_2.id, status='pending_moderation', author=self.partner_employee)
+
+        self.env['mail.message']._notify_moderators()
+        self.assertEmails(False, self.partner_employee | self.partner_employee_2, subject='Message are pending moderation', email_from=self.env.user.company_id.catchall or self.env.user.company_id.email)
diff --git a/addons/test_mail/tests/test_res_users.py b/addons/test_mail/tests/test_res_users.py
new file mode 100644
index 000000000000..4ae3ca002656
--- /dev/null
+++ b/addons/test_mail/tests/test_res_users.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.test_mail.tests import common
+from odoo.tests import tagged
+
+
+@tagged('moderation')
+class TestMessageModeration(common.Moderation):
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestMessageModeration, cls).setUpClass()
+
+    def test_is_moderator(self):
+        self.assertTrue(self.user_employee.is_moderator)
+        self.assertFalse(self.user_admin.is_moderator)
+        self.assertTrue(self.user_employee_2.is_moderator)
+
+    def test_moderation_counter(self):
+        self._create_new_message(self.channel_1.id, status='pending_moderation', author=self.partner_admin)
+        self._create_new_message(self.channel_1.id, status='accepted', author=self.partner_admin)
+        self._create_new_message(self.channel_1.id, status='accepted', author=self.partner_employee)
+        self._create_new_message(self.channel_1.id, status='pending_moderation', author=self.partner_employee)
+        self._create_new_message(self.channel_1.id, status='accepted', author=self.partner_employee_2)
+
+        self.assertEqual(self.user_employee.moderation_counter, 2)
+        self.assertEqual(self.user_employee_2.moderation_counter, 0)
+        self.assertEqual(self.user_admin.moderation_counter, 0)
+
+        self.channel_1.write({'channel_partner_ids': [(4, self.partner_employee_2.id)], 'moderator_ids': [(4, self.user_employee_2.id)]})
+        self.assertEqual(self.user_employee.moderation_counter, 2)
+        self.assertEqual(self.user_employee_2.moderation_counter, 0)
+        self.assertEqual(self.user_admin.moderation_counter, 0)
-- 
GitLab