diff --git a/addons/mail/data/ir_cron_data.xml b/addons/mail/data/ir_cron_data.xml
index 27ae879580492cb2b4d0763e25d8362c13cfcda0..10073110a3aa6de2acebc0df01d20c65da2ea6e8 100644
--- a/addons/mail/data/ir_cron_data.xml
+++ b/addons/mail/data/ir_cron_data.xml
@@ -43,5 +43,17 @@
             <field eval="False" name="doall" />
             <field name="priority">1000</field>
         </record>
+
+        <record id="ir_cron_delete_notification" model="ir.cron">
+            <field name="name">Notification: Delete Notifications older than 6 Month</field>
+            <field name="interval_number">1</field>
+            <field name="interval_type">days</field>
+            <field name="numbercall">-1</field>
+            <field name="doall" eval="False"/>
+            <field name="model_id" ref="model_mail_notification"/>
+            <field name="code">model._gc_notifications(max_age_days=180)</field>
+            <field name="state">code</field>
+        </record>
+
     </data>
 </odoo>
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index 38426d629bdc064a78d000bfad9f82d505526502..a7061c77d3497dcebbb3a7e33eb94c955ee17c8f 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -190,53 +190,28 @@ class Message(models.Model):
     #------------------------------------------------------
 
     @api.model
-    def mark_all_as_read(self, channel_ids=None, domain=None):
-        """ Remove all needactions of the current partner. If channel_ids is
-            given, restrict to messages written in one of those channels. """
+    def mark_all_as_read(self, domain=None):
+        # not really efficient method: it does one db request for the
+        # search, and one for each message in the result set is_read to True in the
+        # current notifications from the relation.
         partner_id = self.env.user.partner_id.id
-        delete_mode = not self.env.user.share  # delete employee notifs, keep customer ones
-        if not domain and delete_mode:
-            query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s"
-            args = [(partner_id,)]
-            if channel_ids:
-                query += """
-                    AND mail_message_id in
-                        (SELECT mail_message_id
-                        FROM mail_message_mail_channel_rel
-                        WHERE mail_channel_id in %s)"""
-                args += [tuple(channel_ids)]
-            query += " RETURNING mail_message_id as id"
-            self._cr.execute(query, args)
-            self.invalidate_cache()
-
-            ids = [m['id'] for m in self._cr.dictfetchall()]
-        else:
-            # not really efficient method: it does one db request for the
-            # search, and one for each message in the result set to remove the
-            # current user from the relation.
-            msg_domain = [('needaction_partner_ids', 'in', partner_id)]
-            if channel_ids:
-                msg_domain += [('channel_ids', 'in', channel_ids)]
-            unread_messages = self.search(expression.AND([msg_domain, domain]))
-            notifications = self.env['mail.notification'].sudo().search([
-                ('mail_message_id', 'in', unread_messages.ids),
-                ('res_partner_id', '=', self.env.user.partner_id.id),
-                ('is_read', '=', False)])
-            if delete_mode:
-                notifications.unlink()
-            else:
-                notifications.write({'is_read': True})
-            ids = unread_messages.mapped('id')
+        msg_domain = [('needaction_partner_ids', 'in', partner_id)]
+        unread_messages = self.search(expression.AND([msg_domain, domain]))
+        ids = unread_messages.ids
+        notifications = self.env['mail.notification'].sudo().search([
+            ('mail_message_id', 'in', ids),
+            ('res_partner_id', '=', partner_id),
+            ('is_read', '=', False)])
+        notifications.write({'is_read': True})
 
-        notification = {'type': 'mark_as_read', 'message_ids': ids, 'channel_ids': channel_ids}
-        self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification)
+        notification = {'type': 'mark_as_read', 'message_ids': ids}
+        self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner_id), notification)
 
         return ids
 
     def set_message_done(self):
         """ Remove the needaction from messages for the current partner. """
         partner_id = self.env.user.partner_id
-        delete_mode = not self.env.user.share  # delete employee notifs, keep customer ones
 
         notifications = self.env['mail.notification'].sudo().search([
             ('mail_message_id', 'in', self.ids),
@@ -264,10 +239,7 @@ class Message(models.Model):
         current_group = [record.id]
         current_channel_ids = record.channel_ids
 
-        if delete_mode:
-            notifications.unlink()
-        else:
-            notifications.write({'is_read': True})
+        notifications.write({'is_read': True})
 
         for (msg_ids, channel_ids) in groups:
             notification = {'type': 'mark_as_read', 'message_ids': msg_ids, 'channel_ids': [c.id for c in channel_ids]}
@@ -501,20 +473,27 @@ class Message(models.Model):
         note_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note')
 
         # fetch notification status
-        notif_dict = {}
-        notifs = self.env['mail.notification'].sudo().search([('mail_message_id', 'in', list(mid for mid in message_tree)), ('res_partner_id', '!=', False), ('is_read', '=', False)])
+
+        notif_dict = defaultdict(lambda: defaultdict(list))
+        notifs = self.env['mail.notification'].sudo().search([('mail_message_id', 'in', list(mid for mid in message_tree)), ('res_partner_id', '!=', False)])
+
         for notif in notifs:
             mid = notif.mail_message_id.id
-            if not notif_dict.get(mid):
-                notif_dict[mid] = {'partner_id': list()}
-            notif_dict[mid]['partner_id'].append(notif.res_partner_id.id)
+            if notif.is_read:
+                notif_dict[mid]['history_partner_ids'].append(notif.res_partner_id.id)
+            else:
+                notif_dict[mid]['needaction_partner_ids'].append(notif.res_partner_id.id)
 
         for message in message_values:
-            message['needaction_partner_ids'] = notif_dict.get(message['id'], dict()).get('partner_id', [])
-            message['is_note'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['id'] == note_id
-            message['is_discussion'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['id'] == com_id
+            message.update({
+                'needaction_partner_ids': notif_dict[message['id']]['needaction_partner_ids'],
+                'history_partner_ids': notif_dict[message['id']]['history_partner_ids'],
+                'is_note': message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['id'] == note_id,
+                'is_discussion': message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['id'] == com_id,
+                'subtype_description': message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description']
+            })
             message['is_notification'] = message['message_type'] == 'user_notification'
-            message['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description']
+
             if message['model'] and self.env[message['model']]._original_module:
                 message['module_icon'] = modules.module.get_module_icon(self.env[message['model']]._original_module)
         return message_values
diff --git a/addons/mail/models/mail_notification.py b/addons/mail/models/mail_notification.py
index c89ce0fd57d5badf9f8cabf58eb83c792405e76a..e47f337e49153892392f8550ad9871cf368a4045 100644
--- a/addons/mail/models/mail_notification.py
+++ b/addons/mail/models/mail_notification.py
@@ -1,5 +1,7 @@
 # -*- coding: utf-8 -*-
 
+from dateutil.relativedelta import relativedelta
+
 from odoo import api, fields, models
 from odoo.tools.translate import _
 
@@ -38,6 +40,7 @@ class Notification(models.Model):
             ("UNKNOWN", "Unknown error"),
             ], string='Failure type')
     failure_reason = fields.Text('Failure reason', copy=False)
+    read_date = fields.Datetime('Read Date', copy=False)
 
     _sql_constraints = [
         # email notification;: partner is required
@@ -57,3 +60,25 @@ class Notification(models.Model):
             return dict(type(self).failure_type.selection).get(self.failure_type, _('No Error'))
         else:
             return _("Unknown error") + ": %s" % (self.failure_reason or '')
+
+    @api.model_create_multi
+    def create(self, vals_list):
+        for vals in vals_list:
+            if vals.get('is_read'):
+                vals['read_date'] = fields.Datetime.now()
+        return super(Notification, self).create(vals_list)
+
+    def write(self, vals):
+        if vals.get('is_read'):
+            vals['read_date'] = fields.Datetime.now()
+        return super(Notification, self).write(vals)
+
+    @api.model
+    def _gc_notifications(self, max_age_days=180):
+        domain = [
+            ('is_read', '=', True),
+            ('read_date', '<', fields.Datetime.now() - relativedelta(days=max_age_days)),
+            ('res_partner_id.partner_share', '=', False),
+            ('notification_type', '=', 'email')
+        ]
+        return self.search(domain).unlink()
diff --git a/addons/mail/static/src/js/discuss_mobile.js b/addons/mail/static/src/js/discuss_mobile.js
index 617700c20c146e2e46416e8dbdbe35bd5339ce91..f90491636a7bd7b705d42439d47d8daaf7cc7302 100644
--- a/addons/mail/static/src/js/discuss_mobile.js
+++ b/addons/mail/static/src/js/discuss_mobile.js
@@ -71,7 +71,7 @@ Discuss.include({
      * @returns {Boolean} true iff we currently are in the Inbox tab
      */
     _isInInboxTab: function () {
-        return _.contains(['mailbox_inbox', 'mailbox_starred'], this._currentState);
+        return _.contains(['mailbox_inbox', 'mailbox_starred', 'mailbox_history'], this._currentState);
     },
     /**
      * @override
@@ -160,7 +160,7 @@ Discuss.include({
      */
     _updateContent: function (type) {
         var self = this;
-        var inMailbox = type === 'mailbox_inbox' || type === 'mailbox_starred';
+        var inMailbox = _.contains(['mailbox_inbox', 'mailbox_starred', 'mailbox_history'], type);
         if (!inMailbox && this._isInInboxTab()) {
             // we're leaving the inbox, so store the thread scrolltop
             this._storeThreadState();
@@ -230,8 +230,8 @@ Discuss.include({
 
             // update bottom buttons
             self.$('.o_mail_mobile_tab').removeClass('active');
-            // mailbox_inbox and mailbox_starred share the same tab
-            type = type === 'mailbox_starred' ? 'mailbox_inbox' : type;
+            // mailbox_inbox, mailbox_starred and mailbox_history share the same tab
+            type = _.contains(['mailbox_inbox', 'mailbox_starred', 'mailbox_history'], type) ? 'mailbox_inbox' : type;
             self.$('.o_mail_mobile_tab[data-type=' + type + ']').addClass('active');
         });
     },
diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js
index d22569461e58b46e3d3a217cec38dc5b4fa21ba1..e5470d219a2869b19863747e729fe5b0a946df7c 100644
--- a/addons/mail/static/src/js/models/messages/message.js
+++ b/addons/mail/static/src/js/models/messages/message.js
@@ -32,6 +32,7 @@ var Message =  AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
      * @param {string} [data.moderation_status='accepted']
      * @param {string} [data.module_icon]
      * @param {Array} [data.needaction_partner_ids = []]
+     * @param {Array} [data.history_partner_ids = []]
      * @param {string} [data.record_name]
      * @param {integer} [data.res_id]
      * @param {Array} [data.starred_partner_ids = []]
@@ -648,6 +649,9 @@ var Message =  AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
         if (_.contains(this._starredPartnerIDs, session.partner_id)) {
             this.setStarred(true);
         }
+        if (_.contains(this._historyPartnerIDs, session.partner_id)) {
+            this._setHistory(true);
+        }
         if (
             this.originatesFromChannel() &&
             _.contains(
@@ -739,6 +743,7 @@ var Message =  AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
      * @param {string} [data.moderation_status='accepted']
      * @param {string} [data.module_icon]
      * @param {Array} [data.needaction_partner_ids = []]
+     * @param {Array} [data.history_partner_ids = []]
      * @param {string} [data.record_name]
      * @param {integer} [data.res_id]
      * @param {Array} [data.starred_partner_ids = []]
@@ -757,6 +762,7 @@ var Message =  AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
         this._moduleIcon = data.module_icon;
         this._needactionPartnerIDs = data.needaction_partner_ids || [];
         this._starredPartnerIDs = data.starred_partner_ids || [];
+        this._historyPartnerIDs = data.history_partner_ids || [];
         this._subject = data.subject;
         this._subtypeDescription = data.subtype_description;
         this._threadIDs = data.channel_ids || [];
@@ -764,6 +770,21 @@ var Message =  AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
 
         this._moderationStatus = data.moderation_status || 'accepted';
     },
+    /*
+     * Set whether the message is history or not.
+     * If it is history, the message is moved to the "History" mailbox.
+     * Note that this function only applies it locally, the server is not aware
+     *
+     * @private
+     * @param {boolean} history if set, the message is history
+     */
+    _setHistory: function (history) {
+        if (history) {
+            this._addThread('mailbox_history');
+        } else {
+            this.removeThread('mailbox_history');
+        }
+    },
     /**
      * Set whether the message is moderated by current user or not.
      * If it is moderated by the current user, the message is moved to the
diff --git a/addons/mail/static/src/js/models/threads/mailbox.js b/addons/mail/static/src/js/models/threads/mailbox.js
index bedd6af27e1652bb92c3cd656c7cae6e00223098..236d960c02dada2077218e65869c884c7a5657f4 100644
--- a/addons/mail/static/src/js/models/threads/mailbox.js
+++ b/addons/mail/static/src/js/models/threads/mailbox.js
@@ -128,7 +128,6 @@ var Mailbox = SearchableThread.extend({
                 model: 'mail.message',
                 method: 'mark_all_as_read',
                 kwargs: {
-                    channel_ids: [],
                     domain: domain,
                 },
             });
@@ -183,6 +182,8 @@ var Mailbox = SearchableThread.extend({
             return [['needaction', '=', true]];
         } else if (this._id === 'mailbox_starred') {
             return [['starred', '=', true]];
+        } else if (this._id === 'mailbox_history') {
+            return [['needaction', '=', false]];
         } else if (this._id === 'mailbox_moderation') {
             return [['need_moderation', '=', true]];
         } else {
diff --git a/addons/mail/static/src/js/models/threads/searchable_thread.js b/addons/mail/static/src/js/models/threads/searchable_thread.js
index 511bc6348c82402ce741c2a2a0b4e00cdb94caf0..74018af1bc0fc29e9ac821193fbc4eb62730d848 100644
--- a/addons/mail/static/src/js/models/threads/searchable_thread.js
+++ b/addons/mail/static/src/js/models/threads/searchable_thread.js
@@ -135,11 +135,12 @@ var SearchableThread = Thread.extend({
      * @override
      * @private
      * @param {mail.model.Message} message
-     * @param {Object} options
+     * @param {Object} [options={}]
      * @param {Array} [options.domain=[]]
      * @param {boolean} [options.incrementUnread=false]
      */
     _addMessage: function (message, options) {
+        options = options || {};
         this._super.apply(this, arguments);
         var cache = this._getCache(options.domain || []);
         var index = _.sortedIndex(cache.messages, message, function (msg) {
diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js
index 6c7cefe42e3e6b871396697f0496052413c29620..2aa083a56d2ec9473e79233963fea3a99b5932f3 100644
--- a/addons/mail/static/src/js/services/mail_manager.js
+++ b/addons/mail/static/src/js/services/mail_manager.js
@@ -1257,7 +1257,7 @@ var MailManager =  AbstractService.extend({
     },
     /**
      * Update the mailboxes with mail data fetched from server, namely 'Inbox',
-     * 'Starred', and 'Moderation Queue' if the user is a moderator of a channel
+     * 'Starred', 'History', and 'Moderation Queue' if the user is a moderator of a channel
      *
      * @private
      * @param {Object} data
@@ -1281,6 +1281,10 @@ var MailManager =  AbstractService.extend({
             name: _t("Starred"),
             mailboxCounter: data.starred_counter || 0,
         });
+        this._addMailbox({
+            id: 'history',
+            name: _t("History"),
+        });
 
         if (data.is_moderator) {
             this._addMailbox({
diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js
index c22276867b1e3adda41722bc1ebfaeaaf43e572d..704e114d8bd5c29be8307e1398032bb38b8c6496 100644
--- a/addons/mail/static/src/js/services/mail_notification_manager.js
+++ b/addons/mail/static/src/js/services/mail_notification_manager.js
@@ -319,12 +319,14 @@ MailManager.include({
      */
     _handlePartnerMarkAsReadNotification: function (data) {
         var self = this;
+        var history = this.getMailbox('history');
         _.each(data.message_ids, function (messageID) {
             var message = _.find(self._messages, function (msg) {
                 return msg.getID() === messageID;
             });
             if (message) {
                 self._removeMessageFromThread('mailbox_inbox', message);
+                history.addMessage(message);
                 self._mailBus.trigger('update_message', message, data.type);
             }
         });
diff --git a/addons/mail/static/src/scss/thread.scss b/addons/mail/static/src/scss/thread.scss
index 89e5f220ea22564dcb2f99d5f7048c807fd2e521..5654020c7836d8b89e4d81f1b98306195a7dc094 100644
--- a/addons/mail/static/src/scss/thread.scss
+++ b/addons/mail/static/src/scss/thread.scss
@@ -229,6 +229,12 @@
         margin-bottom: 20px;
         font-weight: bold;
         font-size: 125%;
+
+        &.o_neutral_face_icon:before {
+            @extend %o-nocontent-init-image;
+            @include size(120px, 140px);
+            background: transparent url(/web/static/src/img/neutral_face.svg) no-repeat center;
+        }
     }
 
     .o_mail_no_content {
diff --git a/addons/mail/static/src/xml/discuss.xml b/addons/mail/static/src/xml/discuss.xml
index afee3a84da20fa5bf36cb6bf476f2db3d77f3f04..71138ae1f24d489853cb185a06842da8560a1b57 100644
--- a/addons/mail/static/src/xml/discuss.xml
+++ b/addons/mail/static/src/xml/discuss.xml
@@ -60,6 +60,10 @@
             <t t-set="counter" t-value="starred.getMailboxCounter()"/>
             <t t-call="mail.discuss.SidebarCounter"/>
         </div>
+        <div t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID === 'mailbox_history') ? 'o_active': ''}"
+            data-thread-id="mailbox_history">
+            <span class="o_thread_name"><i class="fa fa-history mr8"/>History</span>
+        </div>
         <div t-if="isMyselfModerator" t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID == 'mailbox_moderation') ? 'o_active': ''}"
                 data-thread-id="mailbox_moderation">
             <span class="o_thread_name"> <i class="fa fa-envelope mr8"/>Moderation Queue</span>
@@ -242,6 +246,9 @@
                 <button type="button" class="btn btn-secondary d-inline d-md-none o_mailbox_inbox_item" title="Starred" data-type="mailbox_starred">
                     Starred
                 </button>
+                <button type="button" class="btn btn-secondary d-inline d-md-none o_mailbox_inbox_item" title="History" data-type="mailbox_history">
+                    History
+                </button>
             </div>
             <div class="o_mail_discuss_content"/>
             <div class="o_mail_mobile_tabs">
diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml
index dada890149106139bd8fc6c08f1b9c91b927a3bd..ef21b2c5ede7901f8b7f14811b723dc3c592a9ee 100644
--- a/addons/mail/static/src/xml/thread.xml
+++ b/addons/mail/static/src/xml/thread.xml
@@ -128,9 +128,13 @@
             <div>New messages appear here.</div>
         </t>
         <t t-if="thread.getID() === 'mailbox_starred'">
-            <div class="o_thread_title">No starred message</div>
+            <div class="o_thread_title">No starred messages</div>
             <div>You can mark any message as 'starred', and it shows up in this mailbox.</div>
         </t>
+        <t t-if="thread.getID() === 'mailbox_history'">
+            <div class="o_thread_title o_neutral_face_icon">No history messages</div>
+            <div>Messages marked as read will appear in the history.</div>
+        </t>
         <t t-if="thread.getID() === 'mailbox_moderation'">
             <div class="o_thread_title">You have no message to moderate</div>
             <div>Pending moderation messages appear here.</div>
diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js
index 8c65266a392e56e8701fadebe90841f5c97376af..7473194d2b526f4222add6f49f6fc7e46c6a9042 100644
--- a/addons/mail/static/tests/discuss_tests.js
+++ b/addons/mail/static/tests/discuss_tests.js
@@ -50,6 +50,11 @@ QUnit.module('Discuss', {
                         type: 'many2many',
                         relation: 'res.partner',
                     },
+                    history_partner_ids: {
+                        string: "Partners with History",
+                        type: 'many2many',
+                        relation: 'res.partner',
+                    },
                     model: {
                         string: "Related Document model",
                         type: 'char',
@@ -72,6 +77,24 @@ QUnit.module('Discuss', {
                     im_status: 'online',
                 }]
             },
+            'mail.notification': {
+                fields: {
+                    is_read: {
+                        string: "Is Read",
+                        type: 'boolean',
+                    },
+                    mail_message_id: {
+                        string: "Message",
+                        type: 'many2one',
+                        relation: 'mail.message',
+                    },
+                    res_partner_id: {
+                        string: "Needaction Recipient",
+                        type: 'many2one',
+                        relation: 'res.partner',
+                    },
+                },
+            },
         };
         this.services = mailTestUtils.getMailServices();
     },
@@ -83,7 +106,7 @@ QUnit.module('Discuss', {
 });
 
 QUnit.test('basic rendering', async function (assert) {
-    assert.expect(5);
+    assert.expect(6);
 
     var discuss = await createDiscuss({
         id: 1,
@@ -105,11 +128,15 @@ QUnit.test('basic rendering', async function (assert) {
 
     var $inbox = $sidebar.find('.o_mail_discuss_item[data-thread-id=mailbox_inbox]');
     assert.strictEqual($inbox.length, 1,
-        "should have the channel item 'mailbox_inbox' in the sidebar");
+        "should have the mailbox item 'mailbox_inbox' in the sidebar");
 
     var $starred = $sidebar.find('.o_mail_discuss_item[data-thread-id=mailbox_starred]');
     assert.strictEqual($starred.length, 1,
-        "should have the channel item 'mailbox_starred' in the sidebar");
+        "should have the mailbox item 'mailbox_starred' in the sidebar");
+
+    var $history = $sidebar.find('.o_mail_discuss_item[data-thread-id=mailbox_history]');
+    assert.strictEqual($history.length, 1,
+        "should have the mailbox item 'mailbox_history' in the sidebar");
     discuss.destroy();
 });
 
@@ -1490,54 +1517,382 @@ QUnit.test('custom-named DM conversation', async function (assert) {
     discuss.destroy();
 });
 
-QUnit.test('input not cleared on unresolved message_post rpc', async function (assert) {
-    assert.expect(2);
 
-    // Promise to simulate late server response on message post
-    var messagePostPromise = testUtils.makeTestPromise();
+QUnit.test('messages marked as read move to "History" mailbox', async function (assert) {
+    assert.expect(3);
+
+    this.data['mail.message'].records = [{
+        author_id: [5, 'Demo User'],
+        body: '<p>test 1</p>',
+        id: 1,
+        needaction: true,
+        needaction_partner_ids: [3],
+    }, {
+        author_id: [6, 'Test User'],
+        body: '<p>test 2</p>',
+        id: 2,
+        needaction: true,
+        needaction_partner_ids: [3],
+    }];
+    this.data['mail.notification'].records = [{
+        id: 50,
+        is_read: false,
+        mail_message_id: 1,
+        res_partner_id: 3,
+    }, {
+        id: 51,
+        is_read: false,
+        mail_message_id: 1,
+        res_partner_id: 3,
+    }];
 
     this.data.initMessaging = {
-        channel_slots: {
-            channel_channel: [{
-                id: 1,
-                channel_type: "channel",
-                name: "general",
-            }],
+        needaction_inbox_counter: 2,
+    };
+
+    var markAllReadDef = testUtils.makeTestPromise();
+    var objectDiscuss;
+
+    var discuss = await createDiscuss({
+        id: 1,
+        context: {},
+        params: {},
+        data: this.data,
+        services: this.services,
+        session: { partner_id: 3 },
+        mockRPC: function (route, args) {
+            if (args.method === 'mark_all_as_read') {
+                _.each(this.data['mail.message'].records, function (message) {
+                    message.history_partner_ids = [3];
+                    message.needaction_partner_ids = [];
+                });
+                var notificationData = {
+                    type: 'mark_as_read',
+                    message_ids: [1, 2],
+                };
+                var notification = [[false, 'res.partner', 3], notificationData];
+                objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]);
+                markAllReadDef.resolve();
+                return Promise.resolve(3);
+            }
+            return this._super.apply(this, arguments);
         },
+    })
+    objectDiscuss = discuss;
+
+    var $inbox = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_inbox"]');
+    var $history = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_history"]');
+
+    await testUtils.dom.click($history);
+    assert.containsOnce(discuss, '.o_mail_no_content',
+        "should display no content message");
+
+    await testUtils.dom.click($inbox);
+
+    var $markAllReadButton = $('.o_mail_discuss_button_mark_all_read');
+    testUtils.dom.click($markAllReadButton);
+
+    await markAllReadDef;
+    // immediately jump to end of the fadeout animation on messages
+    discuss.$('.o_thread_message').stop(false, true);
+    assert.containsNone(discuss, '.o_thread_message',
+        "there should no message in inbox anymore");
+
+    $history = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_history"]');
+    await testUtils.dom.click($history);
+
+    assert.containsN(discuss, '.o_thread_message', 2,
+        "there should be two messages in History");
+
+    discuss.destroy();
+});
+
+QUnit.test('all messages in "Inbox" in "History" after marked all as read', async function (assert) {
+    assert.expect(10);
+
+    var messagesData = [];
+
+    for (var i = 0; i < 40; i++) {
+        messagesData.push({
+            author_id: [i, 'User ' + i],
+            body: '<p>test ' + i + '</p>',
+            id: i,
+            needaction: true,
+            needaction_partner_ids: [3],
+        });
+    }
+
+    this.data['mail.message'].records = messagesData;
+    this.data.initMessaging = {
+        needaction_inbox_counter: 2,
     };
 
+    var messageFetchCount = 0;
+    var loadMoreDef = testUtils.makeTestPromise();
+    var markAllReadDef = testUtils.makeTestPromise();
+    var objectDiscuss;
+
     var discuss = await createDiscuss({
         id: 1,
         context: {},
         params: {},
         data: this.data,
         services: this.services,
+        session: { partner_id: 3 },
         mockRPC: function (route, args) {
-            if (args.method === 'message_post') {
-                return messagePostPromise;
+            if (args.method === 'mark_all_as_read') {
+                var messageIDs = [];
+                for (var i = 0; i < messagesData.length; i++) {
+                    this.data['mail.message'].records[i].history_partner_ids = [3];
+                    this.data['mail.message'].records[i].needaction_partner_ids = [];
+                    this.data['mail.message'].records[i].needaction = false;
+                    messageIDs.push(i);
+                }
+                var notificationData = {
+                    type: 'mark_as_read',
+                    message_ids: messageIDs,
+                };
+                var notification = [[false, 'res.partner', 3], notificationData];
+                objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]);
+                markAllReadDef.resolve();
+                return Promise.resolve(3);
+            }
+            if (args.method === 'message_fetch') {
+                // 1st message_fetch: 'Inbox' initially
+                // 2nd message_fetch: 'History' initially
+                // 3rd message_fetch: 'History' load more
+                assert.step(args.method);
+
+                messageFetchCount++;
+                if (messageFetchCount === 3) {
+                    loadMoreDef.resolve();
+                }
             }
             return this._super.apply(this, arguments);
         },
     });
+    objectDiscuss = discuss;
 
-    // Click on channel 'general'
-    var $general = discuss.$('.o_mail_discuss_sidebar').find('.o_mail_discuss_item[data-thread-id=1]');
-    await testUtils.dom.click($general);
+    assert.verifySteps(['message_fetch'],
+        "should fetch messages once for needaction messages (Inbox)");
+    assert.containsN(discuss, '.o_thread_message', 30,
+        "there should be 30 messages that are loaded in Inbox");
 
-    // Type message
-    var $input = discuss.$('textarea.o_composer_text_field').first();
-    $input.focus();
-    $input.val('test message');
+    var $markAllReadButton = $('.o_mail_discuss_button_mark_all_read');
 
-    // Send message
-    await testUtils.fields.triggerKeydown($input, 'enter');
-    assert.strictEqual($input.val(), 'test message', "composer should not be cleared on send without server response");
+    await testUtils.dom.click($markAllReadButton);
+    await markAllReadDef;
+
+    // immediately jump to end of the fadeout animation on messages
+    discuss.$('.o_thread_message').stop(false, true);
+    assert.containsNone(discuss, '.o_thread_message',
+        "there should no message in inbox anymore");
+
+    var $history = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_history"]');
+
+    await testUtils.dom.click($history);
+
+    assert.verifySteps(['message_fetch'],
+        "should fetch messages once for history");
+
+    assert.containsN(discuss, '.o_thread_message', 30,
+        "there should be 30 messages in History");
+
+    // simulate a scroll to top to load more messages
+    discuss.$('.o_mail_thread').scrollTop(0);
+
+    await loadMoreDef;
+    await testUtils.nextTick();
+
+    assert.verifySteps(['message_fetch'],
+        "should fetch more messages in history for loadMore");
+    assert.containsN(discuss, '.o_thread_message', 40,
+        "there should be 40 messages in History");
+
+    discuss.destroy();
+});
+
+QUnit.test('messages marked as read move to "History" mailbox', async function (assert) {
+    assert.expect(3);
+
+    this.data['mail.message'].records = [{
+        author_id: [5, 'Demo User'],
+        body: '<p>test 1</p>',
+        id: 1,
+        needaction: true,
+        needaction_partner_ids: [3],
+    }, {
+        author_id: [6, 'Test User'],
+        body: '<p>test 2</p>',
+        id: 2,
+        needaction: true,
+        needaction_partner_ids: [3],
+    }];
+    this.data['mail.notification'].records = [{
+        id: 50,
+        is_read: false,
+        mail_message_id: 1,
+        res_partner_id: 3,
+    }, {
+        id: 51,
+        is_read: false,
+        mail_message_id: 1,
+        res_partner_id: 3,
+    }];
+
+    this.data.initMessaging = {
+        needaction_inbox_counter: 2,
+    };
+
+    const markAllReadDef = testUtils.makeTestPromise();
+    let objectDiscuss;
+
+    const discuss = await createDiscuss({
+        id: 1,
+        context: {},
+        params: {},
+        data: this.data,
+        services: this.services,
+        session: { partner_id: 3 },
+        mockRPC(route, args) {
+            if (args.method === 'mark_all_as_read') {
+                for (const message of (this.data['mail.message'].records)) {
+                    message.history_partner_ids = [3];
+                    message.needaction_partner_ids = [];
+                }
+                const notificationData = {
+                    type: 'mark_as_read',
+                    message_ids: [1, 2],
+                };
+                const notification = [[false, 'res.partner', 3], notificationData];
+                objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]);
+                markAllReadDef.resolve();
+                return Promise.resolve(3);
+            }
+            return this._super.apply(this, arguments);
+        },
+    })
+    objectDiscuss = discuss;
+
+    const $inbox = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_inbox"]');
+    let $history = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_history"]');
+    await testUtils.dom.click($history);
+    assert.containsOnce(discuss, '.o_mail_no_content',
+        "should display no content message");
+
+    await testUtils.dom.click($inbox);
+    var $markAllReadButton = $('.o_mail_discuss_button_mark_all_read');
+    testUtils.dom.click($markAllReadButton);
+    await markAllReadDef;
+    // immediately jump to end of the fadeout animation on messages
+    discuss.$('.o_thread_message').stop(false, true);
+    assert.containsNone(discuss, '.o_thread_message',
+        "there should no message in inbox anymore");
+
+    $history = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_history"]');
+    await testUtils.dom.click($history);
+    assert.containsN(discuss, '.o_thread_message', 2,
+        "there should be two messages in History");
+
+    discuss.destroy();
+});
+
+QUnit.test('all messages in "Inbox" in "History" after marked all as read', async function (assert) {
+    assert.expect(10);
+
+    const messagesData = [];
+    for (let i = 0; i < 40; i++) {
+        messagesData.push({
+            author_id: [i, 'User ' + i],
+            body: '<p>test ' + i + '</p>',
+            id: i,
+            needaction: true,
+            needaction_partner_ids: [3],
+        });
+    }
+
+    this.data['mail.message'].records = messagesData;
+    this.data.initMessaging = {
+        needaction_inbox_counter: 2,
+    };
+
+    let messageFetchCount = 0;
+    const loadMoreDef = testUtils.makeTestPromise();
+    const markAllReadDef = testUtils.makeTestPromise();
+    let objectDiscuss;
+
+    const discuss = await createDiscuss({
+        id: 1,
+        context: {},
+        params: {},
+        data: this.data,
+        services: this.services,
+        session: { partner_id: 3 },
+        mockRPC(route, args) {
+            if (args.method === 'mark_all_as_read') {
+                const messageIDs = [];
+                for (let i = 0; i < messagesData.length; i++) {
+                    this.data['mail.message'].records[i].history_partner_ids = [3];
+                    this.data['mail.message'].records[i].needaction_partner_ids = [];
+                    this.data['mail.message'].records[i].needaction = false;
+                    messageIDs.push(i);
+                }
+                const notificationData = {
+                    type: 'mark_as_read',
+                    message_ids: messageIDs,
+                };
+                const notification = [[false, 'res.partner', 3], notificationData];
+                objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]);
+                markAllReadDef.resolve();
+                return Promise.resolve(3);
+            }
+            if (args.method === 'message_fetch') {
+                // 1st message_fetch: 'Inbox' initially
+                // 2nd message_fetch: 'History' initially
+                // 3rd message_fetch: 'History' load more
+                assert.step(args.method);
+
+                messageFetchCount++;
+                if (messageFetchCount === 3) {
+                    loadMoreDef.resolve();
+                }
+            }
+            return this._super.apply(this, arguments);
+        },
+    });
+    objectDiscuss = discuss;
+
+    assert.verifySteps(['message_fetch'],
+        "should fetch messages once for needaction messages (Inbox)");
+    assert.containsN(discuss, '.o_thread_message', 30,
+        "there should be 30 messages that are loaded in Inbox");
 
-    // Simulate server response
-    messagePostPromise.resolve();
+    const $markAllReadButton = $('.o_mail_discuss_button_mark_all_read');
+    await testUtils.dom.click($markAllReadButton);
+    await markAllReadDef;
+    // immediately jump to end of the fadeout animation on messages
+    discuss.$('.o_thread_message').stop(false, true);
+    assert.containsNone(discuss, '.o_thread_message',
+        "there should no message in inbox anymore");
+
+    const $history = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_history"]');
+    await testUtils.dom.click($history);
+    assert.verifySteps(['message_fetch'],
+        "should fetch messages once for history");
+    assert.containsN(discuss, '.o_thread_message', 30,
+        "there should be 30 messages in History");
+
+    // simulate a scroll to top to load more messages
+    discuss.$('.o_mail_thread').scrollTop(0);
+    await loadMoreDef;
     await testUtils.nextTick();
-    assert.strictEqual($input.val(), '', "composer should be cleared on send after server response");
+    assert.verifySteps(['message_fetch'],
+        "should fetch more messages in history for loadMore");
+    assert.containsN(discuss, '.o_thread_message', 40,
+        "there should be 40 messages in History");
+
     discuss.destroy();
 });
+
 });
 });
diff --git a/addons/test_mail/tests/common.py b/addons/test_mail/tests/common.py
index 752c489cadb2a9f0ce6adaa6bfbfa51e7628c665..d3c41a16ab1677220c26f66656121cfcd3fe628e 100644
--- a/addons/test_mail/tests/common.py
+++ b/addons/test_mail/tests/common.py
@@ -73,7 +73,7 @@ class BaseFunctionalTest(common.SavepointCase):
             for partner_attribute in counters.keys():
                 counter, notif_type, notif_read = counters[partner_attribute]
                 partner = getattr(self, partner_attribute)
-                partner_notif = new_notifications.filtered(lambda n: n.res_partner_id == partner)
+                partner_notif = new_notifications.filtered(lambda n: n.res_partner_id == partner and (n.is_read == (notif_read not in ['unread', ''])))
 
                 self.assertEqual(len(partner_notif), counter)
 
diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py
index 07116885592f93f0cf5ce67fc72f2763e9e36e15..d4181f5b3349233af025399f6121e79eac644a10 100644
--- a/addons/test_mail/tests/test_mail_message.py
+++ b/addons/test_mail/tests/test_mail_message.py
@@ -336,7 +336,7 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
 
         # mark all as read clear needactions
         group_private.message_post(body='Test', message_type='comment', subtype='mail.mt_comment', partner_ids=[emp_partner.id])
-        emp_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
+        emp_partner.env['mail.message'].mark_all_as_read(domain=[])
         na_count = emp_partner.get_needaction_count()
         self.assertEqual(na_count, 0, "mark all as read should conclude all needactions")
 
@@ -353,7 +353,7 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
         na_count = emp_partner.get_needaction_count()
         self.assertEqual(na_count, 1, "message not accessible is currently still counted")
 
-        emp_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
+        emp_partner.env['mail.message'].mark_all_as_read(domain=[])
         na_count = emp_partner.get_needaction_count()
         self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
 
@@ -364,7 +364,7 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
 
         # mark all as read clear needactions
         self.group_pigs.message_post(body='Test', message_type='comment', subtype='mail.mt_comment', partner_ids=[portal_partner.id])
-        portal_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
+        portal_partner.env['mail.message'].mark_all_as_read(domain=[])
         na_count = portal_partner.get_needaction_count()
         self.assertEqual(na_count, 0, "mark all as read should conclude all needactions")
 
@@ -380,7 +380,7 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
         na_count = portal_partner.get_needaction_count()
         self.assertEqual(na_count, 1, "message not accessible is currently still counted")
 
-        portal_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
+        portal_partner.env['mail.message'].mark_all_as_read(domain=[])
         na_count = portal_partner.get_needaction_count()
         self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")