diff --git a/mail_restrict_follower_selection/models/mail_thread.py b/mail_restrict_follower_selection/models/mail_thread.py index 019307988..65e616ae8 100644 --- a/mail_restrict_follower_selection/models/mail_thread.py +++ b/mail_restrict_follower_selection/models/mail_thread.py @@ -1,4 +1,5 @@ from odoo import models +from odoo.tools import config from odoo.tools.safe_eval import safe_eval @@ -11,6 +12,11 @@ class MailThread(models.AbstractModel): result = super(MailThread, self)._message_add_suggested_recipient( result, partner=partner, email=email, reason=reason ) + test_condition = config["test_enable"] and not self.env.context.get( + "test_restrict_follower" + ) + if test_condition or self.env.context.get("no_restrict_follower"): + return result domain = self.env[ "mail.wizard.invite" ]._mail_restrict_follower_selection_get_domain() diff --git a/mail_tracking/README.rst b/mail_tracking/README.rst index aaf357419..d37ee6bfc 100644 --- a/mail_tracking/README.rst +++ b/mail_tracking/README.rst @@ -76,6 +76,9 @@ These are all available status icons: .. |noemail| image:: https://raw.githubusercontent.com/OCA/social/13.0/mail_tracking/static/src/img/no_email.png :width: 10px +.. |anonuser| image:: https://raw.githubusercontent.com/OCA/social/13.0/mail_tracking/static/src/img/anon_user.png + :width: 10px + |unknown| **Unknown**: No email tracking info available. Maybe this notified partner has 'Receive Inbox Notifications by Email' == 'Never' |waiting| **Waiting**: Waiting to be sent @@ -92,6 +95,8 @@ These are all available status icons: |noemail| **No Email**: The partner doesn't have a defined email +|anonuser| **No Partner**: The recipient doesn't have a defined partner + If you want to see all tracking emails and events you can go to diff --git a/mail_tracking/models/__init__.py b/mail_tracking/models/__init__.py index 896721a63..0f2e248a4 100644 --- a/mail_tracking/models/__init__.py +++ b/mail_tracking/models/__init__.py @@ -9,3 +9,4 @@ from . import mail_tracking_event from . import res_partner from . import mail_thread from . import mail_resend_message +from . import mail_alias diff --git a/mail_tracking/models/mail_alias.py b/mail_tracking/models/mail_alias.py new file mode 100644 index 000000000..800259c2c --- /dev/null +++ b/mail_tracking/models/mail_alias.py @@ -0,0 +1,33 @@ +# Copyright 2020 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, models, tools + + +class MailAlias(models.Model): + _inherit = "mail.alias" + + @api.model + @tools.ormcache() + def get_aliases(self): + return { + x["display_name"] + for x in self.search_read([("alias_name", "!=", False)], ["display_name"]) + } + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + self.clear_caches() + return res + + def write(self, vals): + res = super().write(vals) + if "alias_name" in vals: + self.clear_caches() + return res + + def unlink(self): + res = super().unlink() + self.clear_caches() + return res diff --git a/mail_tracking/models/mail_message.py b/mail_tracking/models/mail_message.py index c0c818a05..bc093bb7c 100644 --- a/mail_tracking/models/mail_message.py +++ b/mail_tracking/models/mail_message.py @@ -2,6 +2,8 @@ # Copyright 2019 Alexandre Díaz # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from email.utils import getaddresses + from odoo import _, api, fields, models from odoo.tools import email_split @@ -13,6 +15,7 @@ class MailMessage(models.Model): email_cc = fields.Char( "Cc", help="Additional recipients that receive a " '"Carbon Copy" of the e-mail' ) + email_to = fields.Char("To", help="Raw TO recipients") mail_tracking_ids = fields.One2many( comodel_name="mail.tracking.email", inverse_name="mail_message_id", @@ -105,11 +108,16 @@ class MailMessage(models.Model): .sudo() .search([("mail_message_id", "=", message.id)]) ) - # Get Cc recipients - email_cc_list = email_split(message.email_cc) - if any(email_cc_list): - partners |= partners.search([("email", "in", email_cc_list)]) + # String to List + email_cc_list = self._drop_aliases(email_split(message.email_cc)) + email_to_list = self._drop_aliases(email_split(message.email_to)) + # Search related partners recipients + partners |= partners.search( + [("email", "in", email_cc_list + email_to_list)] + ) + # Operate over set's instead of lists email_cc_list = set(email_cc_list) + email_to_list = set(email_to_list) - email_cc_list # Search all trackings for this message for tracking in trackings: status = self._partner_tracking_status_get(tracking) @@ -127,45 +135,66 @@ class MailMessage(models.Model): } ) if tracking.partner_id: + # Discard mails with tracking email_cc_list.discard(tracking.partner_id.email) + email_to_list.discard(tracking.partner_id.email) partners_already |= tracking.partner_id - # Search all recipients for this message + # Search all partner recipients for this message if message.partner_ids: partners |= message.partner_ids if message.notified_partner_ids: partners |= message.notified_partner_ids - # Remove recipients already included + # Discard partner recipients already included partners -= partners_already - tracking_unkown_values = { + # Default tracking values + tracking_unknown_values = { "status": "unknown", "status_human": self._partner_tracking_status_human_get("unknown"), "error_type": False, "error_description": False, "tracking_id": False, } + # Process tracking status of partner recipients without tracking for partner in partners: + # Discard 'To' with partner + if partner.email in email_to_list: + email_to_list.discard(partner.email) # If there is partners not included, then status is 'unknown' # and perhaps a Cc recipient isCc = False if partner.email in email_cc_list: email_cc_list.discard(partner.email) isCc = True - tracking_unkown_values.update( + tracking_status = tracking_unknown_values.copy() + tracking_status.update( {"recipient": partner.name, "partner_id": partner.id, "isCc": isCc} ) - partner_trackings.append(tracking_unkown_values.copy()) - for email in email_cc_list: - # If there is Cc without partner - tracking_unkown_values.update( - {"recipient": email, "partner_id": False, "isCc": True} - ) - partner_trackings.append(tracking_unkown_values.copy()) + partner_trackings.append(tracking_status) + # Process Cc/To recipients without partner + for cc, lst in [(True, email_cc_list), (False, email_to_list)]: + for email in lst: + tracking_status = tracking_unknown_values.copy() + tracking_status.update( + {"recipient": email, "partner_id": False, "isCc": cc} + ) + partner_trackings.append(tracking_status) res[message.id] = { "partner_trackings": partner_trackings, "is_failed_message": message.is_failed_message, } return res + @api.model + def _drop_aliases(self, mail_list): + aliases = self.env["mail.alias"].get_aliases() + + def _filter_alias(email): + email_wn = getaddresses([email])[0][1] + if email_wn not in aliases: + return email_wn + + return list(filter(_filter_alias, mail_list)) + @api.model def _message_read_dict_postprocess(self, messages, message_tree): """Preare values to be used by the chatter widget""" diff --git a/mail_tracking/models/mail_thread.py b/mail_tracking/models/mail_thread.py index 3b1102bd1..c2718cc4a 100644 --- a/mail_tracking/models/mail_thread.py +++ b/mail_tracking/models/mail_thread.py @@ -33,41 +33,48 @@ class MailThread(models.AbstractModel): def message_post(self, *args, **kwargs): """Adds CC recipient to the message. - Because Odoo implementation avoid store cc recipients we ensure that - this information its written into the mail.message record. + Because Odoo implementation avoid store 'from, to, cc' recipients we + ensure that this information its written into the mail.message record. """ - new_message = super().message_post(*args, **kwargs) - email_cc = kwargs.get("cc") - if email_cc: - new_message.sudo().write({"email_cc": email_cc}) - return new_message + kwargs.update( + {"email_cc": kwargs.get("cc", False), "email_to": kwargs.get("to", False)} + ) + return super().message_post(*args, **kwargs) def _message_get_suggested_recipients(self): - """Adds email Cc recipients as suggested recipients. + """Adds email 'extra' recipients as suggested recipients. If the recipient has a res.partner, use it. """ res = super()._message_get_suggested_recipients() + self._add_extra_recipients_suggestions(res, "email_cc", _("Cc")) + self._add_extra_recipients_suggestions(res, "email_to", _("Anon. To")) + return res + + def _add_extra_recipients_suggestions(self, suggestions, field_mail, reason): ResPartnerObj = self.env["res.partner"] - email_cc_formated_list = [] + aliases = self.env["mail.alias"].get_aliases() + email_extra_formated_list = [] for record in self: - emails_cc = record.message_ids.mapped("email_cc") - for email in emails_cc: - email_cc_formated_list.extend(email_split_and_format(email)) - email_cc_formated_list = set(email_cc_formated_list) - for cc in email_cc_formated_list: - email_parts = getaddresses([cc])[0] - partner_id = record._message_partner_info_from_emails([email_parts[1]])[ - 0 - ].get("partner_id") + emails_extra = record.message_ids.mapped(field_mail) + for email in emails_extra: + email_extra_formated_list.extend(email_split_and_format(email)) + email_extra_formated_list = set(email_extra_formated_list) + email_extra_list = [x[1] for x in getaddresses(email_extra_formated_list)] + partners_info = self._message_partner_info_from_emails(email_extra_list) + for pinfo in partners_info: + partner_id = pinfo["partner_id"] + email = pinfo["full_name"] if not partner_id: - record._message_add_suggested_recipient(res, email=cc, reason=_("Cc")) + if email not in aliases: + self._message_add_suggested_recipient( + suggestions, email=email, reason=reason + ) else: partner = ResPartnerObj.browse(partner_id) - record._message_add_suggested_recipient( - res, partner=partner, reason=_("Cc") + self._message_add_suggested_recipient( + suggestions, partner=partner, reason=reason ) - return res @api.model def _fields_view_get( diff --git a/mail_tracking/readme/USAGE.rst b/mail_tracking/readme/USAGE.rst index aa633338f..2488328ec 100644 --- a/mail_tracking/readme/USAGE.rst +++ b/mail_tracking/readme/USAGE.rst @@ -28,6 +28,9 @@ These are all available status icons: .. |noemail| image:: ../static/src/img/no_email.png :width: 10px +.. |anonuser| image:: ../static/src/img/anon_user.png + :width: 10px + |unknown| **Unknown**: No email tracking info available. Maybe this notified partner has 'Receive Inbox Notifications by Email' == 'Never' |waiting| **Waiting**: Waiting to be sent @@ -44,6 +47,8 @@ These are all available status icons: |noemail| **No Email**: The partner doesn't have a defined email +|anonuser| **No Partner**: The recipient doesn't have a defined partner + If you want to see all tracking emails and events you can go to diff --git a/mail_tracking/static/description/index.html b/mail_tracking/static/description/index.html index 656a86f78..61f9ec00e 100644 --- a/mail_tracking/static/description/index.html +++ b/mail_tracking/static/description/index.html @@ -412,6 +412,7 @@ status icon will appear just right to name of notified partner.</p> <p><img alt="opened" src="https://raw.githubusercontent.com/OCA/social/13.0/mail_tracking/static/src/img/opened.png" style="width: 15px;" /> <strong>Opened</strong>: Opened by partner</p> <p><img alt="cc" src="https://raw.githubusercontent.com/OCA/social/13.0/mail_tracking/static/src/img/cc.png" style="width: 10px;" /> <strong>Cc</strong>: It’s a Carbon-Copy recipient. Can’t know the status so is ‘Unknown’</p> <p><img alt="noemail" src="https://raw.githubusercontent.com/OCA/social/13.0/mail_tracking/static/src/img/no_email.png" style="width: 10px;" /> <strong>No Email</strong>: The partner doesn’t have a defined email</p> +<p><img alt="anonuser" src="https://raw.githubusercontent.com/OCA/social/13.0/mail_tracking/static/src/img/anon_user.png" style="width: 10px;" /> <strong>No Partner</strong>: The recipient doesn’t have a defined partner</p> <p>If you want to see all tracking emails and events you can go to</p> <ul class="simple"> <li>Settings > Technical > Email > Tracking emails</li> diff --git a/mail_tracking/static/src/img/anon_user.png b/mail_tracking/static/src/img/anon_user.png new file mode 100644 index 000000000..e57a7e2e0 Binary files /dev/null and b/mail_tracking/static/src/img/anon_user.png differ diff --git a/mail_tracking/static/src/xml/mail_tracking.xml b/mail_tracking/static/src/xml/mail_tracking.xml index 5144d16ea..37e4d2ebe 100644 --- a/mail_tracking/static/src/xml/mail_tracking.xml +++ b/mail_tracking/static/src/xml/mail_tracking.xml @@ -10,6 +10,11 @@ <i class="fa fa-cc"></i> </span> </t> + <t t-elif="!tracking['isCc'] && !tracking['partner_id']"> + <span class="mail_anon_recipient"> + <i class="fa fa-low-vision"></i> + </span> + </t> <t t-elif="tracking['status'] === 'unknown'"> <span class="mail_tracking_unknown"> <i class="fa fa-ban"></i> diff --git a/mail_tracking/tests/test_mail_tracking.py b/mail_tracking/tests/test_mail_tracking.py index dd849263d..62c22999b 100644 --- a/mail_tracking/tests/test_mail_tracking.py +++ b/mail_tracking/tests/test_mail_tracking.py @@ -9,7 +9,7 @@ import psycopg2 import psycopg2.errorcodes from lxml import etree -from odoo import _, http +from odoo import http from odoo.tests.common import TransactionCase from odoo.tools import mute_logger @@ -152,7 +152,7 @@ class TestMailTracking(TransactionCase): self.assertEqual(tracking_email.error_type, "no_recipient") self.assertFalse(self.recipient.email_bounced) - def _check_partner_trackings(self, message): + def _check_partner_trackings_cc(self, message): message_dict = message.message_format()[0] self.assertEqual(len(message_dict["partner_trackings"]), 3) # mail cc @@ -179,15 +179,16 @@ class TestMailTracking(TransactionCase): "login": "sender-test", } ) + # pylint: disable=C8107 message = self.recipient.with_user(sender_user).message_post( - body=_("<p>This is a test message</p>"), - cc="unnamed@test.com, sender@example.com", + body="<p>This is a test message</p>", + cc="Dominique Pinon <unnamed@test.com>, sender@example.com", ) # suggested recipients recipients = self.recipient._message_get_suggested_recipients() suggested_mails = {email[1] for email in recipients[self.recipient.id]} - self.assertTrue("unnamed@test.com" in suggested_mails) - self.assertEqual(len(recipients[self.recipient.id][0]), 3) + self.assertIn("unnamed@test.com", suggested_mails) + self.assertEqual(len(recipients[self.recipient.id]), 3) # Repeated Cc recipients message = self.env["mail.message"].create( { @@ -198,15 +199,80 @@ class TestMailTracking(TransactionCase): "model": "res.partner", "res_id": self.recipient.id, "partner_ids": [(4, self.recipient.id)], - "email_cc": "unnamed@test.com, sender@example.com" + "email_cc": "Dominique Pinon <unnamed@test.com>, sender@example.com" ", recipient@example.com", "body": "<p>This is another test message</p>", } ) message._moderate_accept() recipients = self.recipient._message_get_suggested_recipients() - self.assertEqual(len(recipients[self.recipient.id][0]), 3) - self._check_partner_trackings(message) + self.assertEqual(len(recipients[self.recipient.id]), 3) + self._check_partner_trackings_cc(message) + + def _check_partner_trackings_to(self, message): + message_dict = message.message_format()[0] + self.assertEqual(len(message_dict["partner_trackings"]), 3) + # mail cc + foundPartner = False + foundNoPartner = False + for tracking in message_dict["partner_trackings"]: + if tracking["partner_id"] == self.sender.id: + foundPartner = True + elif tracking["recipient"] == "support+unnamed@test.com": + foundNoPartner = True + self.assertFalse(tracking["partner_id"]) + self.assertTrue(foundPartner) + self.assertTrue(foundNoPartner) + + def test_email_to(self): + sender_user = self.env["res.users"].create( + { + "name": "Sender User Test", + "partner_id": self.sender.id, + "login": "sender-test", + } + ) + # pylint: disable=C8107 + message = self.recipient.with_user(sender_user).message_post( + body="<p>This is a test message</p>", + to="Dominique Pinon <support+unnamed@test.com>, sender@example.com", + ) + # suggested recipients + recipients = self.recipient._message_get_suggested_recipients() + suggested_mails = {email[1] for email in recipients[self.recipient.id]} + self.assertIn("support+unnamed@test.com", suggested_mails) + self.assertEqual(len(recipients[self.recipient.id]), 3) + # Repeated To recipients + message = self.env["mail.message"].create( + { + "subject": "Message test", + "author_id": self.sender.id, + "email_from": self.sender.email, + "message_type": "comment", + "model": "res.partner", + "res_id": self.recipient.id, + "partner_ids": [(4, self.recipient.id)], + "email_to": "Dominique Pinon <support+unnamed@test.com>" + ", sender@example.com, recipient@example.com", + "body": "<p>This is another test message</p>", + } + ) + message._moderate_accept() + recipients = self.recipient._message_get_suggested_recipients() + self.assertEqual(len(recipients[self.recipient.id]), 3) + self._check_partner_trackings_to(message) + # Catchall + Alias + self.env["ir.config_parameter"].set_param("mail.catchall.domain", "test.com") + self.env["mail.alias"].create( + { + "alias_model_id": self.env["ir.model"]._get("res.partner").id, + "alias_name": "support+unnamed", + } + ) + recipients = self.recipient._message_get_suggested_recipients() + self.assertEqual(len(recipients[self.recipient.id]), 2) + suggested_mails = {email[1] for email in recipients[self.recipient.id]} + self.assertNotIn("support+unnamed@test.com", suggested_mails) def test_failed_message(self): MailMessageObj = self.env["mail.message"]