diff --git a/addons/mail/models/mail_followers.py b/addons/mail/models/mail_followers.py
index 8995de70acf8ac71cffaec0af0b7fef8c30e1a0b..e584b87dd1090139ce1c471a806cf28274bfe116 100644
--- a/addons/mail/models/mail_followers.py
+++ b/addons/mail/models/mail_followers.py
@@ -29,7 +29,7 @@ class Followers(models.Model):
     res_id = fields.Many2oneReference(
         'Related Document ID', index=True, help='Id of the followed resource', model_field='res_model')
     partner_id = fields.Many2one(
-        'res.partner', string='Related Partner', ondelete='cascade', index=True)
+        'res.partner', string='Related Partner', ondelete='cascade', index=True, domain=[('type', '!=', 'private')])
     channel_id = fields.Many2one(
         'mail.channel', string='Listener', ondelete='cascade', index=True)
     subtype_ids = fields.Many2many(
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 8c27eed770341d072e9bcbb6e145eb85d6bcc5fd..4c85ef530441d16f13b94f2295831155d444a4b5 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -2666,9 +2666,9 @@ class MailThread(models.AbstractModel):
             self.check_access_rights('write')
             self.check_access_rule('write')
 
-        # filter inactive
+        # filter inactive and private addresses
         if partner_ids and not adding_current:
-            partner_ids = self.env['res.partner'].sudo().search([('id', 'in', partner_ids), ('active', '=', True)]).ids
+            partner_ids = self.env['res.partner'].sudo().search([('id', 'in', partner_ids), ('active', '=', True), ('type', '!=', 'private')]).ids
 
         return self._message_subscribe(partner_ids, channel_ids, subtype_ids, customer_ids=customer_ids)
 
diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py
index ce1fcb35a239cf6e17e8c59e69eae1ec3ecdbbc2..08b948059d4e52db77e78088543c4bea26de89ab 100644
--- a/addons/mail/models/res_partner.py
+++ b/addons/mail/models/res_partner.py
@@ -130,7 +130,7 @@ class Partner(models.Model):
             If channel_id is given, only members of this channel are returned.
         """
         search_dom = expression.OR([[('name', 'ilike', search)], [('email', 'ilike', search)]])
-        search_dom = expression.AND([[('active', '=', True)], search_dom])
+        search_dom = expression.AND([[('active', '=', True), ('type', '!=', 'private')], search_dom])
         if channel_id:
             search_dom = expression.AND([[('channel_ids', 'in', channel_id)], search_dom])
 
diff --git a/addons/mail/wizard/invite.py b/addons/mail/wizard/invite.py
index 610b1a0dd3e512e2b945500ee2805ed3e8f7f6f6..8b6a2de8342d561615531069b51c85b0ea9ff0c0 100644
--- a/addons/mail/wizard/invite.py
+++ b/addons/mail/wizard/invite.py
@@ -41,7 +41,8 @@ class Invite(models.TransientModel):
 
     res_model = fields.Char('Related Document Model', required=True, index=True, help='Model of the followed resource')
     res_id = fields.Integer('Related Document ID', index=True, help='Id of the followed resource')
-    partner_ids = fields.Many2many('res.partner', string='Recipients', help="List of partners that will be added as follower of the current document.")
+    partner_ids = fields.Many2many('res.partner', string='Recipients', help="List of partners that will be added as follower of the current document.",
+                                   domain=[('type', '!=', 'private')])
     channel_ids = fields.Many2many('mail.channel', string='Channels', help='List of channels that will be added as listeners of the current document.',
                                    domain=[('channel_type', '=', 'channel')])
     message = fields.Html('Message')
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index a75cc2a1367cad7b272041ef626688dcae55bb74..b71a6fe8a74b4ca35ca5e8a46f4ce3a1f701b6c8 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -131,7 +131,8 @@ class MailComposer(models.TransientModel):
                             help='Whether the message is an internal note (comment mode only)')
     partner_ids = fields.Many2many(
         'res.partner', 'mail_compose_message_res_partner_rel',
-        'wizard_id', 'partner_id', 'Additional Contacts')
+        'wizard_id', 'partner_id', 'Additional Contacts',
+        domain=[('type', '!=', 'private')])
     # mass mode options
     notify = fields.Boolean('Notify followers', help='Notify followers of the document (mass post only)')
     auto_delete = fields.Boolean('Delete Emails',
diff --git a/addons/test_mail/tests/test_mail_followers.py b/addons/test_mail/tests/test_mail_followers.py
index 739c210d3e3640dc404248c103b4863e8ea7cc78..9246073aa9760b551756001fec059dec2d4ca904 100644
--- a/addons/test_mail/tests/test_mail_followers.py
+++ b/addons/test_mail/tests/test_mail_followers.py
@@ -174,6 +174,21 @@ class BaseFollowersTest(TestMailCommon):
         self.assertEqual(document.message_partner_ids, self.partner_portal, 'No active test: customer not visible')
         self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal | customer)
 
+    @users('employee')
+    def test_followers_private_address(self):
+        """ Test standard API does not subscribe private addresses """
+        private_address = self.env['res.partner'].sudo().create({
+            'name': 'Private Address',
+            'type': 'private',
+        })
+        document = self.env['mail.test.simple'].browse(self.test_record.id)
+        document.message_subscribe(partner_ids=(self.partner_portal | private_address).ids)
+        self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal)
+
+        # works through low-level API
+        document._message_subscribe(partner_ids=(self.partner_portal | private_address).ids)
+        self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal | private_address)
+
 
 @tagged('mail_followers')
 class AdvancedFollowersTest(TestMailCommon):