diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py
index 1638baae47efbfb3ec7bda61e333e3abc2ab0c3d..1385da9f61eadfb43eaa29b21f23ca37d539bea7 100644
--- a/addons/mail/__manifest__.py
+++ b/addons/mail/__manifest__.py
@@ -12,6 +12,7 @@
     'data': [
         'wizard/invite_view.xml',
         'wizard/mail_compose_message_view.xml',
+        'wizard/mail_resend_message_views.xml',
         'views/mail_message_subtype_views.xml',
         'views/mail_tracking_views.xml',
         'views/mail_message_views.xml',
diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py
index f4b6f6b8d5d7d473c934d13a45cff9e7da33a30f..949a715cddb72164834eac10aa84639784740920 100644
--- a/addons/mail/controllers/main.py
+++ b/addons/mail/controllers/main.py
@@ -224,6 +224,7 @@ class MailController(http.Controller):
             'needaction_inbox_counter': request.env['res.partner'].get_needaction_count(),
             'starred_counter': request.env['res.partner'].get_starred_count(),
             'channel_slots': request.env['mail.channel'].channel_fetch_slot(),
+            'mail_failures': request.env['mail.message'].message_fetch_failed(),
             'commands': request.env['mail.channel'].get_mention_commands(),
             'mention_partner_suggestions': request.env['res.partner'].get_static_mention_suggestions(),
             'shortcodes': request.env['mail.shortcode'].sudo().search_read([], ['source', 'substitution', 'description']),
diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py
index fb4f9b7609622366983f55b00162779fdc3ec0c7..d8abbd1f06f1a9c383598d06041c9a0edcc04286 100644
--- a/addons/mail/models/mail_mail.py
+++ b/addons/mail/models/mail_mail.py
@@ -123,7 +123,7 @@ class MailMail(models.Model):
         return res
 
     @api.multi
-    def _postprocess_sent_message(self, mail_sent=True):
+    def _postprocess_sent_message(self, success_pids, failure_reason=False, failure_type='NONE'):
         """Perform any post-processing necessary after sending ``mail``
         successfully, including deleting it completely along with its
         attachment if the ``auto_delete`` flag of the mail was set.
@@ -131,20 +131,29 @@ class MailMail(models.Model):
 
         :return: True
         """
-        mail_message_notif_ids = [mail.mail_message_id.id for mail in self if mail.notification]
-        if mail_message_notif_ids:
+        notif_mails_ids = [mail.id for mail in self if mail.notification]
+        if notif_mails_ids:
             notifications = self.env['mail.notification'].search([
-                ('mail_message_id', 'in', mail_message_notif_ids),
-                ('is_email', '=', True)])
-            if notifications and mail_sent:
-                notifications.write({
+                ('is_email', '=', True),
+                ('mail_id', 'in', notif_mails_ids),
+                ('email_status', 'not in', ('sent', 'canceled'))
+            ])
+            if notifications:
+                #find all notification linked to a failure
+                failed = self.env['mail.notification']
+                if failure_type != 'NONE':
+                    failed = notifications.filtered(lambda notif: notif.res_partner_id not in success_pids)
+                    failed.write({
+                        'email_status': 'exception',
+                        'failure_type': failure_type,
+                        'failure_reason': failure_reason,
+                    })
+                    messages = notifications.mapped('mail_message_id').filtered(lambda m: m.res_id and m.model)
+                    messages._notify_failure_update()  # notify user that we have a failure
+                (notifications - failed).write({
                     'email_status': 'sent',
                 })
-            elif notifications:
-                notifications.write({
-                    'email_status': 'exception',
-                })
-        if mail_sent:
+        if failure_type in ('NONE', 'RECIPIENT'):  # if we have another error, we want to keep the mail.
             mail_to_delete_ids = [mail.id for mail in self if mail.auto_delete]
             self.browse(mail_to_delete_ids).sudo().unlink()
         return True
@@ -225,7 +234,9 @@ class MailMail(models.Model):
                     # exceptions, it is encapsulated into an Odoo MailDeliveryException
                     raise MailDeliveryException(_('Unable to connect to SMTP Server'), exc)
                 else:
-                    self.browse(batch_ids).write({'state': 'exception', 'failure_reason': exc})
+                    batch = self.browse(batch_ids)
+                    batch.write({'state': 'exception', 'failure_reason': exc})
+                    batch._postprocess_sent_message(success_pids=[], failure_type="SMTP")
             else:
                 self.browse(batch_ids)._send(
                     auto_commit=auto_commit,
@@ -242,13 +253,16 @@ class MailMail(models.Model):
     def _send(self, auto_commit=False, raise_exception=False, smtp_session=None):
         IrMailServer = self.env['ir.mail_server']
         for mail_id in self.ids:
+            success_pids = []
+            failure_type = 'NONE'
+            processing_pid = None
+            mail = None
             try:
                 mail = self.browse(mail_id)
                 if mail.state != 'outgoing':
                     if mail.state != 'exception' and mail.auto_delete:
                         mail.sudo().unlink()
                     continue
-
                 # load attachment binary data with a separate read(), as prefetching all
                 # `datas` (binary field) could bloat the browse cache, triggerring
                 # soft/hard mem limits with temporary data.
@@ -260,7 +274,10 @@ class MailMail(models.Model):
                 if mail.email_to:
                     email_list.append(mail._send_prepare_values())
                 for partner in mail.recipient_ids:
-                    email_list.append(mail._send_prepare_values(partner=partner))
+                    values = mail._send_prepare_values(partner=partner)
+                    values['partner_id'] = partner
+                    email_list.append(values)
+
 
                 # headers
                 headers = {}
@@ -285,8 +302,6 @@ class MailMail(models.Model):
                     'state': 'exception',
                     'failure_reason': _('Error without exception. Probably due do sending an email without computed recipients.'),
                 })
-                mail_sent = False
-
                 # build an RFC2822 email.message.Message object and send it without queuing
                 res = None
                 for email in email_list:
@@ -305,11 +320,16 @@ class MailMail(models.Model):
                         subtype='html',
                         subtype_alternative='plain',
                         headers=headers)
+                    processing_pid = email.pop("partner_id", None)
                     try:
                         res = IrMailServer.send_email(
                             msg, mail_server_id=mail.mail_server_id.id, smtp_session=smtp_session)
+                        if processing_pid:
+                            success_pids.append(processing_pid)
+                        processing_pid = None
                     except AssertionError as error:
                         if str(error) == IrMailServer.NO_VALID_RECIPIENT:
+                            failure_type = "RECIPIENT"
                             # No valid recipient found for this particular
                             # mail item -> ignore error to avoid blocking
                             # delivery to next recipients, if any. If this is
@@ -318,21 +338,19 @@ class MailMail(models.Model):
                                          mail.message_id, email.get('email_to'))
                         else:
                             raise
-                if res:
+                if res:  # mail has been sent at least once, no major exception occured
                     mail.write({'state': 'sent', 'message_id': res, 'failure_reason': False})
-                    mail_sent = True
-
-                # /!\ can't use mail.state here, as mail.refresh() will cause an error
-                # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
-                if mail_sent:
                     _logger.info('Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id)
-                mail._postprocess_sent_message(mail_sent=mail_sent)
+                    # /!\ can't use mail.state here, as mail.refresh() will cause an error
+                    # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
+                mail._postprocess_sent_message(success_pids=success_pids, failure_type=failure_type)
             except MemoryError:
                 # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
                 # instead of marking the mail as failed
                 _logger.exception(
                     'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option',
                     mail.id, mail.message_id)
+                # mail status will stay on ongoing since transaction will be rollback
                 raise
             except psycopg2.Error:
                 # If an error with the database occurs, chances are that the cursor is unusable.
@@ -344,7 +362,7 @@ class MailMail(models.Model):
                 failure_reason = tools.ustr(e)
                 _logger.exception('failed sending mail (id: %s) due to %s', mail.id, failure_reason)
                 mail.write({'state': 'exception', 'failure_reason': failure_reason})
-                mail._postprocess_sent_message(mail_sent=False)
+                mail._postprocess_sent_message(success_pids=success_pids, failure_reason=failure_reason, failure_type='UNKNOWN')
                 if raise_exception:
                     if isinstance(e, AssertionError):
                         # get the args of the original error, wrap into a value and throw a MailDeliveryException
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index d98c98fb1d9b960d490f0ccdd18bfbb26a144fd5..c5d3abda206edd7f801c7d82380fd9a63321c9ae 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -4,11 +4,13 @@
 import logging
 import re
 
+from operator import itemgetter
 from email.utils import formataddr
 
 from odoo import _, api, fields, models, modules, SUPERUSER_ID, tools
 from odoo.exceptions import UserError, AccessError
 from odoo.osv import expression
+from odoo.tools import groupby
 
 _logger = logging.getLogger(__name__)
 _image_dataurl = re.compile(r'(data:image/[a-z]+?);base64,([a-z0-9+/\n]{3,}=*)\n*([\'"])', re.I)
@@ -81,6 +83,9 @@ class Message(models.Model):
     needaction = fields.Boolean(
         'Need Action', compute='_get_needaction', search='_search_needaction',
         help='Need Action')
+    has_error = fields.Boolean(
+        'Has error', compute='_compute_has_error', search='_search_has_error',
+        help='Has error')
     channel_ids = fields.Many2many(
         'mail.channel', 'mail_message_mail_channel_rel', string='Channels')
     # notifications
@@ -114,6 +119,9 @@ class Message(models.Model):
         ('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')
+    #keep notification layout informations to be able to generate mail again
+    layout = fields.Char('Layout', copy=False)  # xml id of layout
+    layout_values = fields.Char('Notification values', copy=False)
 
     @api.multi
     def _get_needaction(self):
@@ -131,6 +139,20 @@ class Message(models.Model):
             return ['&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', False)]
         return ['&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', True)]
 
+    @api.multi
+    def _compute_has_error(self):
+        error_from_notification = self.env['mail.notification'].sudo().search([
+            ('mail_message_id', 'in', self.ids),
+            ('email_status', 'in', ('bounce', 'exception'))]).mapped('mail_message_id')
+        for message in self:
+            message.has_error = message in error_from_notification
+
+    @api.multi
+    def _search_has_error(self, operator, operand):
+        if operator == '=' and operand:
+            return [('notification_ids.email_status', 'in', ('bounce', 'exception'))]
+        return ['!', ('notification_ids.email_status', 'in', ('bounce', 'exception'))]  # this wont work and will be equivalent to "not in" beacause of orm restrictions. Dont use "has_error = False"
+
     @api.depends('starred_partner_ids')
     def _get_starred(self):
         """ Compute if the message is starred by the current user. """
@@ -350,7 +372,7 @@ class Message(models.Model):
                                 if partner.id in partner_tree]
 
             customer_email_data = []
-            for notification in message.notification_ids.filtered(lambda notif: notif.res_partner_id.partner_share and notif.res_partner_id.active):
+            for notification in message.notification_ids.filtered(lambda notif: notif.email_status in ('bounce', 'exception', 'canceled') or (notif.res_partner_id.partner_share and notif.res_partner_id.active)):
                 customer_email_data.append((partner_tree[notification.res_partner_id.id][0], partner_tree[notification.res_partner_id.id][1], notification.email_status))
 
             attachment_ids = []
@@ -375,6 +397,11 @@ class Message(models.Model):
 
         return True
 
+    @api.multi
+    def message_fetch_failed(self):
+        messages = self.search([('has_error', '=', True), ('author_id.id', '=', self.env.user.partner_id.id), ('res_id', '!=', 0), ('model', '!=', False)])
+        return messages._format_mail_failures()
+
     @api.model
     def message_fetch(self, domain, limit=20):
         return self.search(domain, limit=limit).message_format()
@@ -461,6 +488,37 @@ class Message(models.Model):
                 message['module_icon'] = modules.module.get_module_icon(self.env[message['model']]._original_module)
         return message_values
 
+    @api.multi
+    def _format_mail_failures(self):
+        """
+        A shorter message to notify a failure update
+        """
+        failures_infos = []
+        # for each channel, build the information header and include the logged partner information
+        for message in self:
+            info = {
+                'message_id': message.id,
+                'record_name': message.record_name,
+                'model_name': self.env['ir.model']._get(message.model).display_name,
+                'uuid': message.message_id,
+                'res_id': message.res_id,
+                'model': message.model,
+                'last_message_date': message.date,
+                'module_icon': '/mail/static/src/img/smiley/mailfailure.jpg',
+                'notifications': dict((notif.res_partner_id.id, (notif.email_status, notif.res_partner_id.name)) for notif in message.notification_ids)
+            }
+            failures_infos.append(info)
+        return failures_infos
+
+    @api.multi
+    def _notify_failure_update(self):
+        authors = {}
+        for author, author_messages in groupby(self, itemgetter('author_id')):
+            self.env['bus.bus'].sendone(
+                (self._cr.dbname, 'res.partner', author.id),
+                {'type': 'mail_failure', 'elements': self.env['mail.message'].concat(*author_messages)._format_mail_failures()}
+            )
+
     #------------------------------------------------------
     # mail_message internals
     #------------------------------------------------------
@@ -939,8 +997,16 @@ 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
+            
+        # list channels and partner by notification type
+        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'))
+        email_partner = partners_sudo - notif_partners
+
+        #update message, with maybe custom values
         message_values = {}
+        if email_partner:
+            message_values = {'layout': layout, 'layout_values': repr(values)}
         if channels_sudo:
             message_values['channel_ids'] = [(6, 0, channels_sudo.ids)]
         if partners_sudo:
@@ -953,13 +1019,10 @@ class Message(models.Model):
         # notify partners and channels
         # those methods are called as SUPERUSER because portal users posting messages
         # 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:
+        if email_channels or email_partner:
             partners_sudo.search([
                 '|',
-                ('id', 'in', (partners_sudo - notif_partners).ids),
+                ('id', 'in', (email_partner).ids),
                 ('channel_ids', 'in', email_channels.ids),
                 ('email', '!=', self_sudo.author_id.email or self_sudo.email_from),
             ])._notify(self, layout=layout, force_send=force_send, send_after_commit=send_after_commit, values=values)
diff --git a/addons/mail/models/mail_notification.py b/addons/mail/models/mail_notification.py
index e14754b7e8f03135922c6f93fe3433f110786b6d..66f18379565832d59dd63048f8d03f2580392ab1 100644
--- a/addons/mail/models/mail_notification.py
+++ b/addons/mail/models/mail_notification.py
@@ -20,11 +20,35 @@ class Notification(models.Model):
         ('ready', 'Ready to Send'),
         ('sent', 'Sent'),
         ('bounce', 'Bounced'),
-        ('exception', 'Exception')], 'Email Status',
+        ('exception', 'Exception'),
+        ('canceled', 'Canceled')], 'Email Status',
         default='ready', index=True)
+    mail_id = fields.Many2one('mail.mail', 'Mail', index=True)
+    # it would be technically possible to find notification from mail without adding a mail_id field on notification,
+    # comparing partner_ids and message_ids, but this will involve to search notifications one by one since we want to match
+    # bot value. Working with set inclusion, we could have a notif matching message from mail 1 and partner from mail 2, we dont want that.
+    # The solution would be to iterate over mail or to filter mail after search,... or add a mail_id field on notification to KISS
+    failure_type = fields.Selection(selection=[
+            ("NONE", "No error"),
+            ("SMTP", "Error while connecting to smtp server"),
+            ("RECIPIENT", "Invalid email adress"),
+            ("BOUNCE", "Email address not found"),
+            ("UNKNOWN", "Unknown error occured"),
+            ], default='NONE', string='Failure type')
+    failure_reason = fields.Text('Failure reason', copy=False)
 
     @api.model_cr
     def init(self):
         self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_notification_res_partner_id_is_read_email_status_mail_message_id',))
         if not self._cr.fetchone():
             self._cr.execute('CREATE INDEX mail_notification_res_partner_id_is_read_email_status_mail_message_id ON mail_message_res_partner_needaction_rel (res_partner_id, is_read, email_status, mail_message_id)')
+
+    @api.multi
+    def format_failure_reason(self):
+        self.ensure_one()
+        if self.failure_type != 'UNKNOWN':
+            return dict(type(self).failure_type.selection).get(self.failure_type)
+        else:
+            return "Unknow error occured: %s" % (self.failure_reason)
+
+
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 4cde154e1cbf141caa51430871d1d996f8b3320f..4f89da3b69ee94f0383d00dea8f88e8aad7d9442 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -102,6 +102,12 @@ class MailThread(models.AbstractModel):
     message_needaction_counter = fields.Integer(
         'Number of Actions', compute='_get_message_needaction',
         help="Number of messages which requires an action")
+    message_has_error = fields.Boolean(
+        'Message Delivery error', compute='_compute_message_has_error', search='_search_message_has_error',
+        help="If checked, some messages have a delivery error.")
+    message_has_error_counter = fields.Integer(
+        'Number of error', compute='_compute_message_has_error',
+        help="Number of messages with delivery error")
 
     @api.one
     @api.depends('message_follower_ids')
@@ -208,6 +214,26 @@ class MailThread(models.AbstractModel):
     def _search_message_needaction(self, operator, operand):
         return [('message_ids.needaction', operator, operand)]
 
+    @api.multi
+    def _compute_message_has_error(self):
+        self._cr.execute(""" SELECT msg.res_id, COUNT(msg.res_id) FROM mail_message msg
+                             RIGHT JOIN mail_message_res_partner_needaction_rel rel
+                             ON rel.mail_message_id = msg.id AND rel.email_status in ('exception','bounce')
+                             WHERE msg.author_id = %s AND msg.model = %s AND msg.res_id in %s
+                             GROUP BY msg.res_id""",
+                         (self.env.user.partner_id.id, self._name, tuple(self.ids),))
+        res = dict()
+        for result in self._cr.fetchall():
+            res[result[0]] = result[1]
+
+        for record in self:
+            record.message_has_error_counter = res.get(record.id, 0)
+            record.message_has_error = bool(record.message_has_error_counter)
+
+    @api.model
+    def _search_message_has_error(self, operator, operand):
+        return [('message_ids.has_error', operator, operand)]
+
     # ------------------------------------------------------
     # CRUD overrides for automatic subscription and logging
     # ------------------------------------------------------
diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py
index f6472eafd0b1467cdaef14ce655b07e71bf7a6ce..07bee0bb6e6b31ab2aba939ca76b91c238d8babe 100644
--- a/addons/mail/models/res_partner.py
+++ b/addons/mail/models/res_partner.py
@@ -140,6 +140,7 @@ class Partner(models.Model):
                 ('mail_message_id', '=', email.mail_message_id.id),
                 ('res_partner_id', 'in', email.recipient_ids.ids)])
             notifications.write({
+                'mail_id': email.id,
                 'is_email': True,
                 'is_read': True,  # handle by email discards Inbox notification
                 'email_status': 'ready',
diff --git a/addons/mail/static/src/img/smiley/mailfailure.jpg b/addons/mail/static/src/img/smiley/mailfailure.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..6f0ec91ad80ade20e8d7a1e8b081e654cf77d052
Binary files /dev/null and b/addons/mail/static/src/img/smiley/mailfailure.jpg differ
diff --git a/addons/mail/static/src/js/services/chat_manager.js b/addons/mail/static/src/js/services/chat_manager.js
index eaedb9cd11e8b82110142872a9cdb91e21ffcd39..8a90fc57482a6008cd96c79c1e5321d81031066a 100644
--- a/addons/mail/static/src/js/services/chat_manager.js
+++ b/addons/mail/static/src/js/services/chat_manager.js
@@ -119,6 +119,7 @@ var ChatManager =  AbstractService.extend({
 
         this.messages = [];
         this.channels = [];
+        this.messageFailures = [];
         this.channelsPreviewDef;
         this.channelDefs = {};
         this.unreadConversationCounter = 0;
@@ -285,7 +286,8 @@ var ChatManager =  AbstractService.extend({
         var self = this;
         var channelsPreview = _.map(channels, function (channel) {
             var info;
-            if (channel.channel_ids && _.contains(channel.channel_ids,"channel_inbox")) {
+            if ((channel.channel_ids && _.contains(channel.channel_ids, "channel_inbox")) || channel.id === "mail_failure") {
+                // this is a hack to preview messages and mail failure
                 // map inbox(mail_message) data with existing channel/chat template
                 info = _.pick(channel,
                     'id', 'body', 'avatar_src', 'res_id', 'model', 'module_icon',
@@ -299,7 +301,9 @@ var ChatManager =  AbstractService.extend({
                 info.name = info.record_name || info.subject || info.displayed_author;
                 info.image_src = info.module_icon || info.avatar_src;
                 info.message_id = info.id;
-                info.id = 'channel_inbox';
+                if (channel.id !== "mail_failure") {
+                    info.id = 'channel_inbox';
+                }
                 return info;
             }
             info = _.pick(channel, 'id', 'is_chat', 'name', 'status', 'unread_counter');
@@ -420,6 +424,14 @@ var ChatManager =  AbstractService.extend({
         }
         return result;
     },
+    /**
+     * Returns a list of mail failures
+     *
+     * @return {Object[]} list of channels
+     */
+    getMessageFailures: function () {
+        return this.messageFailures;
+    },
     /**
      * Get all listeners of a channel.
      *
@@ -1215,6 +1227,7 @@ var ChatManager =  AbstractService.extend({
             _.each(result.channel_slots, function (channels) {
                 _.each(channels, self._addChannel.bind(self));
             });
+            self.messageFailures = result.mail_failures;
             self.needactionCounter = result.needaction_inbox_counter || 0;
             self.starredCounter = result.starred_counter || 0;
             self.moderationCounter = result.moderation_counter;
@@ -1635,6 +1648,42 @@ var ChatManager =  AbstractService.extend({
             });
         this.chatBus.trigger('update_moderation_counter');
     },
+    /**
+     * Add or remove failure when receiving a failure update message
+     * @private
+     * @param  {Object} data
+     */
+    _manageMessageFailureNotification: function (data) {
+        var self = this;
+        _.each(data.elements, function (updateMessage) {
+
+            var isAddFailure = _.some(updateMessage.notifications, function(notif) {
+                return notif[0] === 'exception' || notif[0] === 'bounce';
+            });
+            var res = _.find(self.messageFailures, {'message_id': updateMessage.message_id});
+            if (res) {
+                var index = _.findIndex(self.messageFailures, res);
+                if (isAddFailure) {
+                    self.messageFailures[index] = updateMessage;
+                } else {
+                    self.messageFailures.splice(index, 1);
+                }
+            } else if (isAddFailure) {
+                self.messageFailures.push(updateMessage);
+            } 
+            var message = _.findWhere(self.messages, { id: updateMessage.message_id });
+            if (message) {
+                if (isAddFailure) {
+                    message.customer_email_status = "exception";
+                } else{
+                    message.customer_email_status = "sent";
+                }
+                self._updateMessageNotificationStatus(updateMessage, message);
+                self.chatBus.trigger('update_message', message);
+            }
+        });
+        this.chatBus.trigger('update_needaction', this.needactionCounter);
+    },
     /**
      * Updates channel_inbox when a message has marked as read.
      *
@@ -1738,6 +1787,8 @@ var ChatManager =  AbstractService.extend({
             this._manageTransientMessageNotification(data);
         } else if (data.type === 'activity_updated') {
             this._manageActivityUpdateNotification(data);
+        } else if (data.type === 'mail_failure') {
+            this._manageMessageFailureNotification(data);
         } else {
             this._manageChatSessionNotification(data);
         }
@@ -1883,7 +1934,28 @@ var ChatManager =  AbstractService.extend({
         channel.unread_counter = counter;
         this.chatBus.trigger("update_channel_unread_counter", channel);
     },
-
+    /**
+     * Update the message notification status of message based on update_message
+     *
+     * @private
+     * @param {Object} updateMessage
+     * @param {Object[]} updateMessage.notifications
+     */
+    _updateMessageNotificationStatus: function (updateMessage, message) {
+        _.each(updateMessage.notifications, function (notif, id, list) {
+            var partnerName = notif[1];
+            var notifStatus = notif[0];
+            var res = _.find(message.customer_email_data, function(entry){ 
+                return entry[0] === parseInt(id);
+            });
+            if (res) {
+                res[2] = notifStatus;
+            } else {
+                message.customer_email_data.push([parseInt(id), partnerName, notifStatus]);
+            }
+        });
+    },
+    
     //--------------------------------------------------------------------------
     // Handlers
     //--------------------------------------------------------------------------
diff --git a/addons/mail/static/src/js/systray.js b/addons/mail/static/src/js/systray.js
index c62b964472f4a77b7e0ac8101ee219c580ad7cc5..f11531b6eabe9ed0d6483d884b3b834179476fd2 100644
--- a/addons/mail/static/src/js/systray.js
+++ b/addons/mail/static/src/js/systray.js
@@ -5,8 +5,8 @@ var config = require('web.config');
 var core = require('web.core');
 var session = require('web.session');
 var SystrayMenu = require('web.SystrayMenu');
+var time = require('web.time');
 var Widget = require('web.Widget');
-
 var QWeb = core.qweb;
 
 /**
@@ -34,19 +34,23 @@ var MessagingMenu = Widget.extend({
         this.$filter_buttons = this.$('.o_filter_button');
         this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels');
         this.filter = false;
-        var chatBus = this.call('chat_manager', 'getChatBus');
-        chatBus.on("update_needaction", this, this.update_counter);
-        chatBus.on("update_channel_unread_counter", this, this.update_counter);
-        this.call('chat_manager', 'isReady').then(this.update_counter.bind(this));
+        var self = this;
+        this.call('chat_manager', 'isReady').then(function () {
+            self.update_counter();
+            var chatBus = self.call('chat_manager', 'getChatBus');
+            chatBus.on('update_needaction', self, self.update_counter);
+            chatBus.on('update_channel_unread_counter', self, self.update_counter);
+        });
         return this._super();
     },
     is_open: function () {
         return this.$el.hasClass('open');
     },
     update_counter: function () {
+        var messageFailures = this.call('chat_manager', 'getMessageFailures');
         var needactionCounter = this.call('chat_manager', 'getNeedactionCounter');
         var unreadConversationCounter = this.call('chat_manager', 'getUnreadConversationCounter');
-        var counter =  needactionCounter + unreadConversationCounter;
+        var counter =  needactionCounter + unreadConversationCounter + messageFailures.length;
         this.$('.o_notification_counter').text(counter);
         this.$el.toggleClass('o_no_notification', !counter);
         if (this.is_open()) {
@@ -55,7 +59,6 @@ var MessagingMenu = Widget.extend({
     },
     update_channels_preview: function () {
         var self = this;
-
         // Display spinner while waiting for channels preview
         this.$channels_preview.html(QWeb.render('Spinner'));
         this.call('chat_manager', 'isReady').then(function () {
@@ -69,6 +72,28 @@ var MessagingMenu = Widget.extend({
                     return channel.type !== 'static';
                 }
             });
+            var formatedFailures = [];
+            var messageFailures = self.call('chat_manager', 'getMessageFailures');
+            _.each(messageFailures, function (messageFailure) {
+                var model = messageFailure.model;
+                var existingFailure = _.findWhere(formatedFailures, { model: model });
+                if (existingFailure) {
+                    if (existingFailure.res_id !== messageFailure.res_id) {
+                        existingFailure.res_id = null;
+                        existingFailure.record_name = messageFailure.model_name;//for display, will be used as subject
+                    }
+                    existingFailure.unread_counter ++;
+                } else {
+                    messageFailure = _.clone(messageFailure);
+                    messageFailure.unread_counter = 1;
+                    messageFailure.id = "mail_failure";
+                    messageFailure.body = "An error occured sending an email";
+                    messageFailure.date = moment(time.str_to_datetime(messageFailure.last_message_date));
+                    messageFailure.displayed_author = "";
+                    formatedFailures.push(messageFailure);
+                }
+            });
+            channels = _.union(channels, formatedFailures);
             self.call('chat_manager', 'getMessages', {channelID: 'channel_inbox'})
                 .then(function (messages) {
                     var res = [];
@@ -115,8 +140,10 @@ var MessagingMenu = Widget.extend({
         this.call('chat_window_manager', 'openChat');
     },
 
-    // Handlers
 
+    //--------------------------------------------------------------------------
+    // Handlers
+    //--------------------------------------------------------------------------
     /**
      * When a channel is clicked on, we want to open chat/channel window
      *
@@ -126,7 +153,7 @@ var MessagingMenu = Widget.extend({
     _onClickChannel: function (event) {
         var self = this;
         var channelID = $(event.currentTarget).data('channel_id');
-        if (channelID === 'channel_inbox') {
+        if (channelID === 'channel_inbox' || channelID === 'mail_failure') {
             var resID = $(event.currentTarget).data('res_id');
             var resModel = $(event.currentTarget).data('res_model');
             if (resModel && resID) {
@@ -136,6 +163,16 @@ var MessagingMenu = Widget.extend({
                     views: [[false, 'form']],
                     res_id: resID
                 });
+            } else if (resModel) {
+                this.do_action({
+                    name: "Mail failures",
+                    type: 'ir.actions.act_window',
+                    view_mode: 'kanban,list,form',
+                    views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
+                    target: 'current',
+                    res_model: resModel,
+                    domain: [['message_has_error', '=', true]],
+                });
             } else {
                 this.do_action('mail.mail_channel_action_client_chat', {clear_breadcrumbs: true})
                     .then(function () {
diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js
index 39d179a715a35ddd257d9442e7d384ed036c199c..54a36e169e4051469587490971d9f201fbd24454 100644
--- a/addons/mail/static/src/js/thread.js
+++ b/addons/mail/static/src/js/thread.js
@@ -35,15 +35,16 @@ var Thread = Widget.extend({
         "click .o_attachment_download": "_onAttachmentDownload",
         "click .o_attachment_view": "_onAttachmentView",
         "click .o_thread_message_needaction": function (event) {
-            var message_id = $(event.currentTarget).data('message-id');
-            this.trigger("mark_as_read", message_id);
+            var messageId = $(event.currentTarget).data('message-id');
+            this.trigger("mark_as_read", messageId);
         },
         "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);
+            var messageId = $(event.currentTarget).data('message-id');
+            this.trigger("toggle_star_status", messageId);
         },
+        "click .o_thread_message_email_exception": "_onClickMailException",
         "click .o_thread_message_reply": function (event) {
             this.selected_id = $(event.currentTarget).data('message-id');
             this.$('.o_thread_message').removeClass('o_thread_selected_message');
@@ -414,6 +415,18 @@ var Thread = Widget.extend({
         var decision = $button.data('decision');
         this.trigger_up('message_moderation', { messageID: messageID, decision: decision });
     },
+    /**
+     * @private
+     * @param {MouseEvent} event
+     */
+    _onClickMailException: function (event) {
+        var messageId = $(event.currentTarget).data('message-id');
+        this.do_action('mail.mail_resend_message_action', {
+            additional_context: { 
+                mail_message_to_resend: messageId 
+            }
+        });
+    },
 });
 
 Thread.ORDER = ORDER;
diff --git a/addons/mail/static/src/scss/thread.scss b/addons/mail/static/src/scss/thread.scss
index 0529238a36e84dc6cb71fdb5e82d4159b3334aa5..6d8eb8b8254bea44c26bac0b4188a0fd147d27b2 100644
--- a/addons/mail/static/src/scss/thread.scss
+++ b/addons/mail/static/src/scss/thread.scss
@@ -161,6 +161,7 @@
                 &.o_thread_message_email_exception {
                     color: red;
                     opacity: 1;
+                    cursor: pointer;
                 }
                 &.o_thread_message_email_bounce {
                     color: red;
diff --git a/addons/mail/static/src/xml/discuss.xml b/addons/mail/static/src/xml/discuss.xml
index 1ec08a236356c77438fb477c83ac8f9cf1a32ca1..1eda1dfec1bd88a4ca59b8d529e99940223d8dc9 100644
--- a/addons/mail/static/src/xml/discuss.xml
+++ b/addons/mail/static/src/xml/discuss.xml
@@ -231,7 +231,9 @@
                         <span class="fa fa-mail-reply"/> You:
                     </t>
                     <t t-else="">
-                        <t t-esc="channel.last_message.displayed_author"/>:
+                        <t t-if="channel.last_message.displayed_author">
+                            <t t-esc="channel.last_message.displayed_author"/>:
+                        </t>
                     </t>
                     <t t-raw="channel.last_message_preview"/>
                 </div>
diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml
index 20049e89cf6a0050a489175be6bd355606aad2bd..ca8ae9b8319a1c9e69020b626189da2534005e48 100644
--- a/addons/mail/static/src/xml/thread.xml
+++ b/addons/mail/static/src/xml/thread.xml
@@ -175,14 +175,19 @@
                         (from <a t-att-data-oe-id="message.origin_id" href="#">#<t t-esc="message.origin_name"/></a>)
                     </t>
                     <span t-if="options.display_email_icon and message.customer_email_data and message.customer_email_data.length" class="o_thread_tooltip_container">
-                        <i t-att-class="'o_thread_tooltip o_thread_message_email o_thread_message_email_' + message.customer_email_status + ' fa fa-envelope-o'"/>
+                        <t t-set="fatype" t-value="''"/>
+                        <t t-if="message.customer_email_status === 'sent' or message.customer_email_status === 'ready'">
+                            <t t-set="fatype" t-value="'-o'"/>
+                        </t>
+                        <i t-att-class="'o_thread_tooltip o_thread_message_email o_thread_message_email_' + message.customer_email_status + ' fa fa-envelope'+ fatype" t-att-data-message-id="message.id"/>
                         <span class="o_thread_tooltip_content">
                             <t t-foreach="message.customer_email_data" t-as="customer">
                                 <span>
-                                    <t t-if="customer[2] == 'sent'"><i class='fa fa-check'/></t>
-                                    <t t-if="customer[2] == 'bounce'"><i class='fa fa-exclamation'/></t>
-                                    <t t-if="customer[2] == 'exception'"><i class='fa fa-exclamation'/></t>
-                                    <t t-if="customer[2] == 'ready'"><i class='fa fa-send-o'/></t>
+                                    <t t-if="customer[2] === 'sent'"><i class='fa fa-check'/></t>
+                                    <t t-if="customer[2] === 'bounce'"><i class='fa fa-exclamation'/></t>
+                                    <t t-if="customer[2] === 'exception'"><i class='fa fa-exclamation'/></t>
+                                    <t t-if="customer[2] === 'ready'"><i class='fa fa-send-o'/></t>
+                                    <t t-if="customer[2] === 'canceled'"><i class='fa fa-trash-o'/></t>
                                     <t t-esc="customer[1]"/>
                                 </span>
                                 <br />
diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js
index 22c1783f5a3aaecedcd0184768f915b45312f87b..773e457015211bbb2d343ed7e8df0e8982fc003a 100644
--- a/addons/mail/static/tests/helpers/mock_server.js
+++ b/addons/mail/static/tests/helpers/mock_server.js
@@ -56,6 +56,7 @@ MockServer.include({
             'mention_partner_suggestions': [],
             'shortcodes': [],
             'menu_id': false,
+            'mail_failures': [],
         });
     },
 
diff --git a/addons/mail/wizard/__init__.py b/addons/mail/wizard/__init__.py
index 819ae4154f469da3cccd84c5ab670d86bceb8806..e6ba124f4ae7187d58652bb443b5dc535f019257 100644
--- a/addons/mail/wizard/__init__.py
+++ b/addons/mail/wizard/__init__.py
@@ -3,6 +3,7 @@
 
 from . import invite
 from . import mail_compose_message
+from . import mail_resend_message
 from . import email_template_preview
 from . import base_module_uninstall
 from . import base_partner_merge
diff --git a/addons/mail/wizard/mail_resend_message.py b/addons/mail/wizard/mail_resend_message.py
new file mode 100644
index 0000000000000000000000000000000000000000..b4480220e4fcf0d0ca87044d0b6c8dc60e0fcd1a
--- /dev/null
+++ b/addons/mail/wizard/mail_resend_message.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import _, api, fields, models
+from ast import literal_eval
+from odoo.exceptions import UserError
+
+class MailResendMessage(models.TransientModel):
+    _name = 'mail.resend.message'
+    _description = 'Email resend wizard'
+
+    mail_message_id = fields.Many2one('mail.message', 'Message', readonly=True)
+    partner_ids = fields.One2many('mail.resend.partner', 'resend_wizard_id', string='Recipients')
+    notification_ids = fields.Many2many('mail.notification', string='Notifications', readonly=True)
+    has_cancel = fields.Boolean(compute='_compute_has_cancel')
+    partner_readonly = fields.Boolean(compute='_compute_partner_readonly')
+
+    @api.depends("partner_ids")
+    def _compute_has_cancel(self):
+        self.has_cancel = self.partner_ids.filtered(lambda p: not p.resend)
+
+    def _compute_partner_readonly(self):
+        self.partner_readonly = not self.env['res.partner'].check_access_rights('write', raise_exception=False)
+
+    @api.model
+    def default_get(self, fields):
+        rec = super(MailResendMessage, self).default_get(fields)
+        message_id = self._context.get('mail_message_to_resend')
+        if message_id:
+            mail_message_id = self.env['mail.message'].browse(message_id)
+            notification_ids = mail_message_id.notification_ids.filtered(lambda notif: notif.email_status in ('exception', 'bounce'))
+            partner_readonly = not self.env['res.partner'].check_access_rights('write', raise_exception=False)
+            partner_ids = [(0, 0,
+                {
+                    "partner_id": notif.res_partner_id.id,
+                    "name": notif.res_partner_id.name,
+                    "email": notif.res_partner_id.email,
+                    "resend": True,
+                    "message": notif.format_failure_reason(),
+                }
+                ) for notif in notification_ids]
+            rec['partner_readonly'] = partner_readonly
+            rec['notification_ids'] = [(6, 0, notification_ids.ids)]
+            rec['mail_message_id'] = mail_message_id.id
+            rec['partner_ids'] = partner_ids
+        else:
+            raise UserError('No message_id found in context')
+        return rec
+
+    @api.multi
+    def resend_mail_action(self):
+        """ Process the wizard content and proceed with sending the related
+            email(s), rendering any template patterns on the fly if needed. """
+        for wizard in self:
+            "If a partner disappeared from partner list, we cancel the notification"
+            to_cancel = wizard.partner_ids.filtered(lambda p: not p.resend).mapped("partner_id")
+            to_send = wizard.partner_ids.filtered(lambda p: p.resend).mapped("partner_id")
+            notif_to_cancel = wizard.notification_ids.filtered(lambda notif: notif.res_partner_id in to_cancel and notif.email_status in ('exception', 'bounce'))
+            notif_to_cancel.sudo().write({'email_status': 'canceled'})
+            to_send.sudo()._notify(self.mail_message_id, self.mail_message_id.layout, force_send=True, send_after_commit=False, values=literal_eval(self.mail_message_id.layout_values))
+            self.mail_message_id._notify_failure_update()
+        return {'type': 'ir.actions.act_window_close'}
+
+    @api.multi
+    def cancel_mail_action(self):
+        for wizard in self:
+            for notif in wizard.notification_ids:
+                notif.filtered(lambda notif: notif.email_status in ('exception', 'bounce')).sudo().write({'email_status': 'canceled'})
+            wizard.mail_message_id._notify_failure_update()
+        return {'type': 'ir.actions.act_window_close'}
+
+
+
+class PartnerResend(models.TransientModel):
+    _name = 'mail.resend.partner'
+    _description = 'Partner with additionnal information for mail resend'
+
+    partner_id = fields.Many2one('res.partner', string='Partner', required=True, ondelete='cascade')
+    name = fields.Char(related="partner_id.name")
+    email = fields.Char(related="partner_id.email")
+    resend = fields.Boolean(string="Send Again", default=True)
+    resend_wizard_id = fields.Many2one('mail.resend.message', string="Resend wizard")
+    message = fields.Char(string="Help message")
+
diff --git a/addons/mail/wizard/mail_resend_message_views.xml b/addons/mail/wizard/mail_resend_message_views.xml
new file mode 100644
index 0000000000000000000000000000000000000000..85af6dc4b0960eab4407483d0869a05e7d1a1548
--- /dev/null
+++ b/addons/mail/wizard/mail_resend_message_views.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <record id="mail_resend_message_view_form" model="ir.ui.view">
+            <field name="name">mail.resend.message.form</field>
+            <field name="model">mail.resend.message</field>
+            <field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
+            <field name="arch" type="xml">
+                <form string="Edit Partners">
+                    <field name="mail_message_id" invisible="1"/>
+                    <field name="notification_ids" invisible="1"/>
+                    <field name="has_cancel" invisible="1"/>
+                    <field name="partner_readonly" invisible="1"/>
+                    <p>Please, select the action to do on each mail and correct the email address if needed. The modified address will be saved on the corresponding contact.</p>
+                    <field name="partner_ids">
+                        <tree string="Recipient" editable="top" create="0" delete="0">
+                            <field name="name" readonly="1"/>
+                            <field name="email" attrs="{'readonly': [('parent.partner_readonly', '=', True)]}"/>
+                            <field name="message" readonly="1"/>
+                            <field name="resend" widget="boolean_toggle"/>
+                        </tree>
+                    </field>
+                    <p attrs="{'invisible':[('has_cancel', '==', False)]}"> <span class="fa fa-info-circle"/> Caution: It won't be possible to send this mail again to the recipients you did not select.</p>
+                    <footer>
+                        <button string="Resend to selected" name="resend_mail_action" type="object" class="btn-primary o_mail_send"/>
+                        <button string="Ignore all failure" name="cancel_mail_action" type="object" class="btn-default" />
+                        <button string="Cancel" class="btn-default" special="cancel" />
+                    </footer>
+                </form>
+            </field>
+        </record>
+        <record id="mail_resend_message_action" model="ir.actions.act_window">
+            <field name="name">Resend mail</field>
+            <field name="res_model">mail.resend.message</field>
+            <field name="type">ir.actions.act_window</field>
+            <field name="view_type">form</field>
+            <field name="view_mode">form</field>
+            <field name="target">new</field>
+        </record>
+        
+    </data>
+</odoo>
diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py
index 2de2b3ec4b12aa5d1ef15127fca3479b8f104c7c..beb4952da1fe6b9c260cbf7dda1cb16a382fcb0f 100644
--- a/addons/mass_mailing/models/mail_mail.py
+++ b/addons/mass_mailing/models/mail_mail.py
@@ -95,11 +95,13 @@ class MailMail(models.Model):
         return res
 
     @api.multi
-    def _postprocess_sent_message(self, mail_sent=True):
+    def _postprocess_sent_message(self, failure_type='NONE', **kwargs):
+        mail_sent = failure_type == 'NONE'  # we consider that a recipient error is a failure with mass mailling and show them as failed
         for mail in self:
             if mail.mailing_id:
                 if mail_sent is True and mail.statistics_ids:
                     mail.statistics_ids.write({'sent': fields.Datetime.now(), 'exception': False})
                 elif mail_sent is False and mail.statistics_ids:
                     mail.statistics_ids.write({'exception': fields.Datetime.now()})
-        return super(MailMail, self)._postprocess_sent_message(mail_sent=mail_sent)
+        return super(MailMail, self)._postprocess_sent_message(failure_type=failure_type, **kwargs)
+    
\ No newline at end of file
diff --git a/addons/test_mail/tests/__init__.py b/addons/test_mail/tests/__init__.py
index 411d73d5694a28035e139cef78ed674213185758..2d63b95f205c580afc1b0d768c8708c0dc9a21d5 100644
--- a/addons/test_mail/tests/__init__.py
+++ b/addons/test_mail/tests/__init__.py
@@ -4,6 +4,7 @@ from . import test_mail_activity
 from . import test_mail_followers
 from . import test_mail_message
 from . import test_mail_mail
+from . import test_mail_resend
 from . import test_mail_channel
 from . import test_mail_gateway
 from . import test_mail_template
diff --git a/addons/test_mail/tests/test_mail_resend.py b/addons/test_mail/tests/test_mail_resend.py
new file mode 100644
index 0000000000000000000000000000000000000000..2179006248891d2882634997506cfc9ccb35b35b
--- /dev/null
+++ b/addons/test_mail/tests/test_mail_resend.py
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import itertools
+from unittest.mock import patch
+
+from odoo.addons.test_mail.tests import common
+from odoo.tests import tagged
+from odoo.tools import mute_logger
+from odoo import api
+from odoo.addons.base.models.ir_mail_server import IrMailServer
+
+
+@tagged('resend_test')
+class TestMailResend(common.BaseFunctionalTest, common.MockEmails):
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestMailResend, cls).setUpClass()
+        #Two users
+        cls.user1 = cls.env['res.users'].with_context(cls._quick_create_user_ctx).create({
+            'name': 'Employee 1',
+            'login': 'e1',
+            'email': 'e1',  # invalid email
+            'notification_type': 'email',
+            'groups_id': [(6, 0, [cls.env.ref('base.group_user').id])]
+        })
+        cls.user2 = cls.env['res.users'].with_context(cls._quick_create_user_ctx).create({
+            'name': 'Employee 2',
+            'login': 'e2',
+            'email': 'e2@example.com',
+            'notification_type': 'email',
+            'groups_id': [(6, 0, [cls.env.ref('base.group_user').id])]
+        })
+        #Two partner
+        cls.partner1 = cls.env['res.partner'].with_context(cls._quick_create_user_ctx).create({
+            'name': 'Partner 1',
+            'email': 'p1'  # invalid email
+        })
+        cls.partner2 = cls.env['res.partner'].with_context(cls._quick_create_user_ctx).create({
+            'name': 'Partner 2',
+            'email': 'p2@example.com'
+        })
+
+        @api.model
+        def send_email(self, message, *args, **kwargs):
+            assert '@' in message['To'], self.NO_VALID_RECIPIENT
+            return message['Message-Id']
+        cls.bus_update_failure = []
+        def sendone(self, channel, message):
+            if 'type' in message and message['type'] == 'mail_failure':
+                cls.bus_update_failure.append((channel, message))
+        cls.env['ir.mail_server']._patch_method('send_email', send_email)
+        cls.env['bus.bus']._patch_method('sendone', sendone)
+        cls.partners = cls.env['res.partner'].concat(cls.user1.partner_id, cls.user2.partner_id, cls.partner1, cls.partner2)
+        cls.invalid_email_partners = cls.env['res.partner'].concat(cls.user1.partner_id, cls.partner1)
+
+    def setUp(self):
+        super(TestMailResend, self).setUp()
+        TestMailResend.bus_update_failure = []
+
+    def assertNotifStates(self, states, message):
+        notif = self.env['mail.notification'].search([('mail_message_id', '=', message.id)], order="res_partner_id asc")
+        self.assertEquals(tuple(notif.mapped('email_status')), states)
+        return notif
+
+    def assertBusMessage(self, partners):
+        partner_ids = [elem[0][2] for elem in self.bus_update_failure]
+        self.assertEquals(partner_ids, [partner.id for partner in partners])
+        self.bus_update_failure.clear()
+
+    @classmethod
+    def tearDownClass(cls):
+        # Remove mocks
+        cls.env['ir.mail_server']._revert_method('send_email')
+        cls.env['bus.bus']._revert_method('sendone')
+        super(TestMailResend, cls).tearDownClass()
+
+    def assertEmails(self, *args, **kwargs):
+        res = super(TestMailResend, self).assertEmails(*args, **kwargs)
+        self._mails.clear()
+
+    @mute_logger('odoo.addons.mail.models.mail_mail')
+    def test_mail_resend_workflow(self):
+        cls = TestMailResend
+        #missconfigured server
+        @api.model
+        def connect_failure(**kwargs):
+            raise Exception
+        with patch.object(IrMailServer, 'connect', side_effect=connect_failure):
+            message = self.test_record.sudo().message_post(partner_ids=self.partners.ids, subtype='mail.mt_comment', message_type='notification')
+        self.assertBusMessage([self.partner_admin])
+        self.assertEmails(self.partner_admin, [])
+        self.assertNotifStates(('exception', 'exception', 'exception', 'exception'), message)
+        wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({})
+        self.assertEqual(wizard.notification_ids.mapped('res_partner_id'), self.partners, "wizard should manage notifications for each failed partner")
+        wizard.resend_mail_action()
+        self.assertBusMessage([self.partner_admin] * 3)  # three more failure sent on bus, one for each mail in failure and one for resend
+        self.assertEmails(self.partner_admin, self.partners)
+        self.assertNotifStates(('exception', 'sent', 'exception', 'sent'), message)
+        self.user1.write({"email": 'u1@example.com'})
+        self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({}).resend_mail_action()
+        self.assertBusMessage([self.partner_admin] * 2)  # two more failure update sent on bus, one for failed mail and one for resend
+        self.assertEmails(self.partner_admin, self.invalid_email_partners)
+        self.assertNotifStates(('sent', 'sent', 'exception', 'sent'), message)
+        self.partner1.write({"email": 'p1@example.com'})
+        self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({}).resend_mail_action()
+        self.assertBusMessage([self.partner_admin])  # A success update should be sent on bus once the email has no more failure
+        self.assertEmails(self.partner_admin, self.partner1)
+        self.assertNotifStates(('sent', 'sent', 'sent', 'sent'), message)
+
+    @mute_logger('odoo.addons.mail.models.mail_mail')
+    def test_mail_send_no_failure(self):
+        self.user1.write({"email": 'u1@example.com'})
+        self.partner1.write({"email": 'p1@example.com'})
+        message = self.test_record.sudo().message_post(partner_ids=self.partners.ids, subtype='mail.mt_comment', message_type='notification')
+        self.assertBusMessage([])  # one update for cancell
+
+    @mute_logger('odoo.addons.mail.models.mail_mail')
+    def test_remove_mail_become_canceled(self):
+        message = self.test_record.sudo().message_post(partner_ids=self.partners.ids, subtype='mail.mt_comment', message_type='notification')
+        self.assertEmails(self.partner_admin, self.partners)
+        self.assertBusMessage([self.partner_admin] * 2)  # two failure sent on bus, one for each mail
+        wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({})
+        partners = wizard.partner_ids.mapped("partner_id")
+        self.assertEqual(self.invalid_email_partners, partners)
+        wizard.partner_ids.filtered(lambda p: p.partner_id == self.partner1).write({"resend": False})
+        wizard.resend_mail_action()
+        self.assertEmails(self.partner_admin, self.user1)
+        self.assertBusMessage([self.partner_admin] * 2)  # two more failure sent on bus, one for failure, one for resend
+        self.assertNotifStates(('exception', 'sent', 'canceled', 'sent'), message)
+
+    @mute_logger('odoo.addons.mail.models.mail_mail')
+    def test_cancel_all(self):
+        message = self.test_record.sudo().message_post(partner_ids=self.partners.ids, subtype='mail.mt_comment', message_type='notification')
+        self.assertNotifStates(('exception', 'sent', 'exception', 'sent'), message)
+        self.assertBusMessage([self.partner_admin] * 2)
+        wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({})
+        wizard.cancel_mail_action()
+        self.assertNotifStates(('canceled', 'sent', 'canceled', 'sent'), message)
+        self.assertBusMessage([self.partner_admin])  # one update for cancell