diff --git a/.oca/oca-port/blacklist/mail_tracking.json b/.oca/oca-port/blacklist/mail_tracking.json new file mode 100644 index 0000000000..1ab17caca8 --- /dev/null +++ b/.oca/oca-port/blacklist/mail_tracking.json @@ -0,0 +1,6 @@ +{ + "pull_requests": { + "792": "(auto) Nothing to port from PR #792", + "1244": "(auto) Nothing to port from PR #1244" + } +} diff --git a/mail_tracking/__manifest__.py b/mail_tracking/__manifest__.py index 2dbaf72c84..bb7db76411 100644 --- a/mail_tracking/__manifest__.py +++ b/mail_tracking/__manifest__.py @@ -23,6 +23,7 @@ "views/mail_tracking_event_view.xml", "views/mail_message_view.xml", "views/res_partner_view.xml", + "views/res_config_settings.xml", ], "assets": { "web.assets_backend": [ diff --git a/mail_tracking/models/__init__.py b/mail_tracking/models/__init__.py index bf42270a08..a4cd6cbf92 100644 --- a/mail_tracking/models/__init__.py +++ b/mail_tracking/models/__init__.py @@ -1,5 +1,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import res_company +from . import res_config_settings from . import ir_mail_server from . import mail_bounced_mixin from . import mail_guest diff --git a/mail_tracking/models/mail_message.py b/mail_tracking/models/mail_message.py index 00d8722eee..996d269568 100644 --- a/mail_tracking/models/mail_message.py +++ b/mail_tracking/models/mail_message.py @@ -226,6 +226,12 @@ def tracking_status(self): @api.model def _drop_aliases(self, mail_list): aliases = self.env["mail.alias"].get_aliases() + if self.env.company.mail_tracking_show_aliases: + IrConfigParamObj = self.env["ir.config_parameter"].sudo() + aliases = "{}@{}".format( + IrConfigParamObj.get_param("mail.catchall.alias"), + IrConfigParamObj.get_param("mail.catchall.domain"), + ) def _filter_alias(email): email_wn = getaddresses([email])[0][1] diff --git a/mail_tracking/models/mail_thread.py b/mail_tracking/models/mail_thread.py index c17b0eeca2..6198c0a60e 100644 --- a/mail_tracking/models/mail_thread.py +++ b/mail_tracking/models/mail_thread.py @@ -94,9 +94,10 @@ def _add_extra_recipients_suggestions(self, suggestions, field_mail, reason): ) else: partner = ResPartnerObj.browse(partner_id) - self._message_add_suggested_recipient( - suggestions, partner=partner, reason=reason - ) + if partner.email not in aliases: + self._message_add_suggested_recipient( + suggestions, partner=partner, reason=reason + ) @api.model def get_view(self, view_id=None, view_type="form", **options): diff --git a/mail_tracking/models/mail_tracking_email.py b/mail_tracking/models/mail_tracking_email.py index a42e48fe7a..cda2f335e3 100644 --- a/mail_tracking/models/mail_tracking_email.py +++ b/mail_tracking/models/mail_tracking_email.py @@ -118,8 +118,7 @@ class MailTrackingEmail(models.Model): @api.depends("mail_message_id") def _compute_message_id(self): """This helper field will allow us to map the message_id from either the linked - mail.message or a mass.mailing mail.trace. - """ + mail.message or a mass.mailing mail.trace.""" self.message_id = False for tracking in self.filtered("mail_message_id"): tracking.message_id = tracking.mail_message_id.message_id @@ -462,3 +461,40 @@ def event_process(self, request, post, metadata, event_type=None): # - return 'NONE' if this request is not for you # - return 'ERROR' if any error return "NONE" # pragma: no cover + + def _get_old_mail_tracking_email_domain(self, max_age_days): + target_write_date = fields.Datetime.subtract( + fields.Datetime.now(), days=max_age_days + ) + return [("write_date", "<", target_write_date)] + + @api.autovacuum + def _gc_mail_tracking_email(self, limit=5000): + config_max_age_days = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("mail_tracking.mail_tracking_email_max_age_days") + ) + try: + max_age_days = int(config_max_age_days) + except ValueError: + max_age_days = 0 + + if not max_age_days > 0: + return False + + domain = self._get_old_mail_tracking_email_domain(max_age_days) + records_to_delete = self.search(domain, limit=limit).exists() + if records_to_delete: + _logger.info( + "Deleting %s mail.tracking.email records", len(records_to_delete) + ) + records_to_delete.flush_recordset() + # Using a direct query to avoid ORM as it causes an issue with + # a related field mass_mailing_id in customer DB when deleting + # the records. This might be 14.0 specific, so changing to + # .unlink() should be tested when forward porting. + query = "DELETE FROM mail_tracking_email WHERE id IN %s" + args = (tuple(records_to_delete.ids),) + self.env.cr.execute(query, args) + self.invalidate_model() diff --git a/mail_tracking/models/res_company.py b/mail_tracking/models/res_company.py new file mode 100644 index 0000000000..afdcce95bc --- /dev/null +++ b/mail_tracking/models/res_company.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + mail_tracking_show_aliases = fields.Boolean( + string="Show Aliases in Mail Tracking", + default=False, + ) diff --git a/mail_tracking/models/res_config_settings.py b/mail_tracking/models/res_config_settings.py new file mode 100644 index 0000000000..3136ae4ab1 --- /dev/null +++ b/mail_tracking/models/res_config_settings.py @@ -0,0 +1,16 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + mail_tracking_show_aliases = fields.Boolean( + related="company_id.mail_tracking_show_aliases", + readonly=False, + ) + mail_tracking_email_max_age_days = fields.Integer( + "Max age in days of mail tracking email records", + config_parameter="mail_tracking.mail_tracking_email_max_age_days", + help="If set as positive integer enables the deletion of " + "old mail tracking records to reduce the database size.", + ) diff --git a/mail_tracking/static/description/index.html b/mail_tracking/static/description/index.html index e1602b01d1..12253cdf81 100644 --- a/mail_tracking/static/description/index.html +++ b/mail_tracking/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -495,7 +496,9 @@

Contributors

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

diff --git a/mail_tracking/tests/__init__.py b/mail_tracking/tests/__init__.py index d40d444b68..76c005840b 100644 --- a/mail_tracking/tests/__init__.py +++ b/mail_tracking/tests/__init__.py @@ -2,3 +2,4 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import test_mail_tracking +from . import test_gc_mail_tracking_email diff --git a/mail_tracking/tests/test_gc_mail_tracking_email.py b/mail_tracking/tests/test_gc_mail_tracking_email.py new file mode 100644 index 0000000000..74f32cefed --- /dev/null +++ b/mail_tracking/tests/test_gc_mail_tracking_email.py @@ -0,0 +1,88 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields + +from odoo.addons.base.tests.common import SavepointCaseWithUserDemo + + +class TestMailTrackingEmailCleanUp(SavepointCaseWithUserDemo): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.settings = cls.env["res.config.settings"].create( + {"mail_tracking_email_max_age_days": 365} + ) + cls.settings.set_values() + cls.partner = cls.env.ref("base.res_partner_address_28") + cls.message = cls.env["mail.message"].create( + { + "model": "res.partner", + "res_id": cls.partner.id, + "body": "TEST", + "message_type": "email", + "subtype_id": cls.env.ref("mail.mt_comment").id, + "author_id": cls.partner.id, + "date": "2024-03-26", + } + ) + cls.recent_mail_tracking_email = cls.env["mail.tracking.email"].create( + {"mail_message_id": cls.message.id} + ) + # Can't set the write_date directly as it gets overwritten by the ORM + cls.old_mail_tracking_email = cls.env["mail.tracking.email"].create( + {"mail_message_id": cls.message.id} + ) + cls.total_count = 2 + cls.recent_count = 1 + cls.domain = [ + ("mail_message_id", "=", cls.message.id), + ] + + def _set_write_date(self): + # Set the write_date of the old record to be older than the max_age_days + # Update DB directly to avoid ORM overwriting the write_date + old_write_date = fields.Datetime.subtract(fields.Datetime.now(), days=400) + self.env.cr.execute( + "UPDATE mail_tracking_email SET write_date = %s WHERE id = %s", + (old_write_date, self.old_mail_tracking_email.id), + ) + + def test_deletion_of_mail_tracking_email(self): + self._set_write_date() + self.assertEqual( + len(self.env["mail.tracking.email"].search(self.domain)), self.total_count + ) + self.env["mail.tracking.email"]._gc_mail_tracking_email() + self.assertEqual( + len(self.env["mail.tracking.email"].search(self.domain)), self.recent_count + ) + self.assertTrue(self.recent_mail_tracking_email.exists()) + + def test_deletion_follows_configuration_variable(self): + self._set_write_date() + self.assertEqual( + len(self.env["mail.tracking.email"].search(self.domain)), self.total_count + ) + # when disabled, no deletions should happen + self.settings.mail_tracking_email_max_age_days = 0 + self.settings.set_values() + self.env["mail.tracking.email"]._gc_mail_tracking_email() + self.assertEqual( + len(self.env["mail.tracking.email"].search(self.domain)), self.total_count + ) + # when disabled, no deletions should happen + self.settings.mail_tracking_email_max_age_days = -1 + self.settings.set_values() + self.env["mail.tracking.email"]._gc_mail_tracking_email() + self.assertEqual( + len(self.env["mail.tracking.email"].search(self.domain)), self.total_count + ) + # when enabled, deletions should happen + self.settings.mail_tracking_email_max_age_days = 365 + self.settings.set_values() + self.env["mail.tracking.email"]._gc_mail_tracking_email() + self.assertEqual( + len(self.env["mail.tracking.email"].search(self.domain)), self.recent_count + ) diff --git a/mail_tracking/tests/test_mail_tracking.py b/mail_tracking/tests/test_mail_tracking.py index 99d525f777..e07a873f8b 100644 --- a/mail_tracking/tests/test_mail_tracking.py +++ b/mail_tracking/tests/test_mail_tracking.py @@ -158,6 +158,36 @@ def test_message_post_partner_no_email(self): self.assertEqual(tracking_email.error_type, "no_recipient") self.assertFalse(self.recipient.email_bounced) + def test_message_post_show_aliases(self): + # Create message with show aliases setup + self.env.company.mail_tracking_show_aliases = True + # Setup catchall domain + IrConfigParamObj = self.env["ir.config_parameter"].sudo() + IrConfigParamObj.set_param("mail.catchall.domain", "test.com") + # pylint: disable=C8107 + 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_cc": "Dominique Pinon , customer-invoices@test.com", # noqa E501 + "body": "

This is another test message

", + } + ) + message_dict, *_ = message.message_format() + self.assertTrue( + any( + [ + tracking["recipient"] == "customer-invoices@test.com" + for tracking in message_dict["partner_trackings"] + ] + ) + ) + def _check_partner_trackings_cc(self, message): message_dict = message.message_format()[0] self.assertEqual(len(message_dict["partner_trackings"]), 3) diff --git a/mail_tracking/views/res_config_settings.xml b/mail_tracking/views/res_config_settings.xml new file mode 100644 index 0000000000..cad361fd90 --- /dev/null +++ b/mail_tracking/views/res_config_settings.xml @@ -0,0 +1,35 @@ + + + + res.config.settings.view.form.inherit.mail.tracking + res.config.settings + + + + + +
+
+
+ + +
+
+
+
+
+
+