diff --git a/mail_hide_inline_attachments/README.rst b/mail_hide_inline_attachments/README.rst new file mode 100644 index 0000000000..58c2459f32 --- /dev/null +++ b/mail_hide_inline_attachments/README.rst @@ -0,0 +1,194 @@ +============================== +Mail - Hide Inline Attachments +============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:96103eaaa45f84a91e31f2a1d29942d30ccc480f01199926c1df3171c12daf1d + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github + :target: https://github.com/OCA/social/tree/16.0/mail_hide_inline_attachments + :alt: OCA/social +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/social-16-0/social-16-0-mail_hide_inline_attachments + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/social&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module hides inline images from emails in the attachment list of records that +inherit from ``mail.thread`` and also from the ``attachment_ids`` field of the +``mail.message`` model. + +When an email contains embedded (inline) images, they are converted into attachments by +Odoo and become visible both in the email body and in the attachment list of the record +and message. This module filters these inline images so they appear only in the email +body. + +Features +-------- + +- Automatically detects attachments that are referenced inline in message bodies + through: + - CID (Content-ID) references in ```` tags + - ``data-filename`` attributes in ```` tags + - ``/web/image/{id}`` URLs in the ``src`` attribute of ```` tags +- Filters these attachments from the record's attachment list (via ``mail.thread``) +- Filters these attachments from the ``attachment_ids`` field of ``mail.message`` +- Images remain visible in the email body +- Works with all models that inherit from ``mail.thread`` + +How it works +------------ + +The module intercepts attachment processing at two points: + +1. **During message creation** (``mail.thread._message_post_process_attachments``): + Inline attachments are unlinked from the record (``res_model`` and ``res_id`` are + cleared), but remain linked to the message. +2. **During message formatting** (``mail.message._message_format``): Inline attachments + are filtered from the attachment list returned to the web client. + + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Configuration +============= + +This module does not require additional configuration. It works automatically after +installation. + +Inline attachment detection +---------------------------- + +The module detects inline attachments through three methods: + +1. **CID (Content-ID)**: When an ```` tag has ``src="cid:xxx"``, the attachment with + that CID is considered inline +2. **data-filename**: When an ```` tag has the ``data-filename="filename"`` attribute, + the attachment with that name is considered inline +3. **URL /web/image/{id}**: When an ```` tag has ``src="/web/image/123"``, the + attachment with ID 123 is considered inline + +All these methods are automatically checked during message processing. + + +Usage +===== + +Usage +===== + +After installation, the module works automatically. No additional configuration is +required. + +Behavior +-------- + +When an email is received or sent with inline images: + +1. Images are processed normally and appear in the email body +2. Corresponding attachments are created in the system +3. Inline attachments are automatically filtered and do not appear: + - In the record's attachment list (chatter) + - In the ``attachment_ids`` field of ``mail.message`` +4. Only attachments that are not inline images remain visible in the attachment list + +Example +------- + +If an email contains: + +- 1 inline image (company logo) +- 2 normal attachments (PDF and DOCX) + +The result will be: + +- The inline image appears only in the email body +- The 2 normal attachments appear in the record's and message's attachment list + + +Known issues / Roadmap +====================== + +Roadmap +======= + +Planned future improvements: + +- Support for other types of inline content besides images +- Configuration option to disable inline attachment filtering +- Improvements in inline attachment detection for edge cases +- Support for inline attachments in threaded replies + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* KMEE + +Contributors +~~~~~~~~~~~~ + +Contributors +============ + +- `KMEE `_: + - Luis Felipe Miléo + + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +.. |maintainer-mileo| image:: https://github.com/mileo.png?size=40px + :target: https://github.com/mileo + :alt: mileo + +Current `maintainer `__: + +|maintainer-mileo| + +This module is part of the `OCA/social `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mail_hide_inline_attachments/__init__.py b/mail_hide_inline_attachments/__init__.py new file mode 100644 index 0000000000..7a67aee29b --- /dev/null +++ b/mail_hide_inline_attachments/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2024 - KMEE +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html + +from . import models diff --git a/mail_hide_inline_attachments/__manifest__.py b/mail_hide_inline_attachments/__manifest__.py new file mode 100644 index 0000000000..318f6a734f --- /dev/null +++ b/mail_hide_inline_attachments/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright (C) 2024 - KMEE +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html + +{ + "name": "Mail - Hide Inline Attachments", + "summary": "Hide inline email images from attachment list", + "version": "16.0.1.0.0", + "category": "Discuss", + "author": "KMEE, Odoo Community Association (OCA)", + "maintainers": ["mileo"], + "website": "https://github.com/OCA/social", + "license": "LGPL-3", + "depends": [ + "mail", + ], + "data": [], + "installable": True, + "auto_install": False, + "application": False, +} diff --git a/mail_hide_inline_attachments/models/__init__.py b/mail_hide_inline_attachments/models/__init__.py new file mode 100644 index 0000000000..30d7983c4c --- /dev/null +++ b/mail_hide_inline_attachments/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2024 - KMEE +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html + +from . import mail_thread +from . import mail_message diff --git a/mail_hide_inline_attachments/models/mail_message.py b/mail_hide_inline_attachments/models/mail_message.py new file mode 100644 index 0000000000..b3b11c33b8 --- /dev/null +++ b/mail_hide_inline_attachments/models/mail_message.py @@ -0,0 +1,90 @@ +# Copyright (C) 2024 - KMEE +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html + +import logging +import re + +import lxml.html + +from odoo import models +from odoo.tools import ustr + +_logger = logging.getLogger(__name__) + + +class MailMessage(models.Model): + _inherit = "mail.message" + + def _get_inline_attachment_ids(self): + """Identifica IDs de anexos que são referenciados inline no body. + + Retorna um set com os IDs dos anexos que aparecem como imagens + inline no body da mensagem (referências /web/image/{id}). + """ + self.ensure_one() + inline_attachment_ids = set() + + if not self.body: + return inline_attachment_ids + + try: + root = lxml.html.fromstring(ustr(self.body)) + for node in root.iter("img"): + src = node.get("src", "") + # Pattern matches: + # - /web/image/{id} + # - /web/image/{id}?access_token=... + # - /web/image/{id}/... + matches = re.findall(r"/web/image/(\d+)", src) + for mid in matches: + inline_attachment_ids.add(int(mid)) + except Exception as e: + _logger.warning( + "Erro ao processar body para detectar anexos inline " + "na mensagem %d: %s", + self.id, + e, + ) + + return inline_attachment_ids + + def _message_format(self, fnames, format_reply=True, legacy=False): + """Override para filtrar anexos inline do campo attachment_ids. + + Anexos que são referenciados inline no body (imagens) não devem + aparecer na lista de anexos do mail.message. + """ + vals_list = super()._message_format( + fnames, format_reply=format_reply, legacy=legacy + ) + + # Processa em batch para melhor performance + message_ids = [vals["id"] for vals in vals_list] + messages = self.browse(message_ids) + + for vals in vals_list: + message_id = vals["id"] + message = messages.filtered(lambda m, mid=message_id: m.id == mid) + if not message: + continue + + inline_attachment_ids = message._get_inline_attachment_ids() + + if inline_attachment_ids and vals.get("attachment_ids"): + # Filtra anexos inline da lista de anexos formatados + filtered_attachments = [ + att + for att in vals["attachment_ids"] + if att.get("id") not in inline_attachment_ids + ] + vals["attachment_ids"] = filtered_attachments + _logger.debug( + "Filtrados %d anexos inline da mensagem %d " + "(IDs: %s). Restaram %d anexos.", + len(inline_attachment_ids), + message.id, + sorted(inline_attachment_ids), + len(filtered_attachments), + ) + + return vals_list diff --git a/mail_hide_inline_attachments/models/mail_thread.py b/mail_hide_inline_attachments/models/mail_thread.py new file mode 100644 index 0000000000..8719036876 --- /dev/null +++ b/mail_hide_inline_attachments/models/mail_thread.py @@ -0,0 +1,192 @@ +# Copyright (C) 2024 - KMEE +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html + +import logging +import re + +import lxml.html + +from odoo import models +from odoo.tools import ustr + +_logger = logging.getLogger(__name__) + + +class MailThread(models.AbstractModel): + _inherit = "mail.thread" + + def _collect_inline_references(self, body): + """Collect CIDs and filenames referenced in body. + + Returns a tuple (inline_cids, inline_names) with sets of + referenced CIDs and filenames. + """ + inline_cids = set() + inline_names = set() + if not body: + return inline_cids, inline_names + + try: + root = lxml.html.fromstring(ustr(body)) + for node in root.iter("img"): + src = node.get("src", "") + # Check for CID references (cid:xxx) + if src.startswith("cid:"): + cid = src.split("cid:")[1] + inline_cids.add(cid) + # Check for data-filename attribute + filename = node.get("data-filename") + if filename: + inline_names.add(filename) + except Exception as e: + _logger.warning("Erro ao processar body para detectar anexos inline: %s", e) + + return inline_cids, inline_names + + def _identify_inline_attachments( + self, attachments, inline_cids, inline_names, model, res_id + ): + """Identify which attachments are inline based on CID or filename. + + Returns a set of indices of inline attachments. + """ + inline_indices = set() + if not (inline_cids or inline_names): + return inline_indices + + for idx, attachment in enumerate(attachments): + if len(attachment) < 2: + continue + name = attachment[0] + info = attachment[2] if len(attachment) >= 3 else None + cid = info and info.get("cid") + if cid and cid in inline_cids: + inline_indices.add(idx) + _logger.info( + "Anexo inline detectado por CID [%s] ID %s: %s (CID: %s)", + model, + res_id, + name, + cid, + ) + elif name in inline_names: + inline_indices.add(idx) + _logger.info( + "Anexo inline detectado por filename [%s] ID %s: %s", + model, + res_id, + name, + ) + + return inline_indices + + def _find_inline_attachments_from_body(self, processed_body, body, new_attachments): + """Find inline attachments from /web/image/{id} references in body. + + Returns a recordset of inline attachments found in the body. + """ + inline_attachments = self.env["ir.attachment"].sudo() + body_to_check = processed_body or body + if not body_to_check: + return inline_attachments + + try: + root = lxml.html.fromstring(ustr(body_to_check)) + for node in root.iter("img"): + src = node.get("src", "") + matches = re.findall(r"/web/image/(\d+)", src) + for mid in matches: + attachment_id = int(mid) + matching_att = new_attachments.filtered( + lambda a, aid=attachment_id: a.id == aid + ) + if matching_att: + inline_attachments |= matching_att + except Exception as e: + _logger.warning("Erro ao processar body processado: %s", e) + + return inline_attachments + + def _unlink_inline_attachments(self, inline_attachments, model, res_id): + """Unlink inline attachments from the record.""" + if not inline_attachments: + return + + _logger.info( + "Desvinculando %d anexos inline do registro [%s] ID %s: %s", + len(inline_attachments), + model, + res_id, + sorted(inline_attachments.mapped("id")), + ) + # Unlink inline attachments from the record + # They will remain linked only to the message + inline_attachments.write( + { + "res_model": False, + "res_id": False, + } + ) + + def _message_post_process_attachments( + self, attachments, attachment_ids, message_values + ): + """Override to prevent inline attachments from being linked to the record. + + Inline attachments (with CID or referenced in body) will be created + without res_model and res_id, so they won't appear in the attachment + list of the record. They will still be accessible via the message body. + """ + body = message_values.get("body") + model = message_values.get("model") + res_id = message_values.get("res_id") + + # Collect CIDs and filenames referenced in body BEFORE processing + inline_cids, inline_names = self._collect_inline_references(body) + + # Identify which attachments are inline based on CID or filename + inline_indices = self._identify_inline_attachments( + attachments, inline_cids, inline_names, model, res_id + ) + + # Process attachments normally first + return_values = super()._message_post_process_attachments( + attachments, attachment_ids, message_values + ) + + if not inline_indices or not model or not res_id: + return return_values + + # Get newly created attachments from return values + m2m_attachment_ids = return_values.get("attachment_ids", []) + new_attachment_ids = [ + cmd[1] + for cmd in m2m_attachment_ids + if isinstance(cmd, tuple) and cmd[0] == 4 + ] + + if not new_attachment_ids: + return return_values + + # Map inline indices to attachment IDs + # Note: attachments list order should match new_attachments order + new_attachments = self.env["ir.attachment"].sudo().browse(new_attachment_ids) + + # Find which of the new attachments correspond to inline ones + # We need to match by position in the list + inline_attachments = self.env["ir.attachment"].sudo() + for idx in inline_indices: + if idx < len(new_attachments): + inline_attachments |= new_attachments[idx] + + # Also check body after processing for /web/image/{id} references + processed_body = return_values.get("body") + inline_from_body = self._find_inline_attachments_from_body( + processed_body, body, new_attachments + ) + inline_attachments |= inline_from_body + + # Unlink inline attachments from the record + self._unlink_inline_attachments(inline_attachments, model, res_id) + + return return_values diff --git a/mail_hide_inline_attachments/readme/CONFIGURE.rst b/mail_hide_inline_attachments/readme/CONFIGURE.rst new file mode 100644 index 0000000000..3e1ee97766 --- /dev/null +++ b/mail_hide_inline_attachments/readme/CONFIGURE.rst @@ -0,0 +1,20 @@ +Configuration +============= + +This module does not require additional configuration. It works automatically after +installation. + +Inline attachment detection +---------------------------- + +The module detects inline attachments through three methods: + +1. **CID (Content-ID)**: When an ```` tag has ``src="cid:xxx"``, the attachment with + that CID is considered inline +2. **data-filename**: When an ```` tag has the ``data-filename="filename"`` attribute, + the attachment with that name is considered inline +3. **URL /web/image/{id}**: When an ```` tag has ``src="/web/image/123"``, the + attachment with ID 123 is considered inline + +All these methods are automatically checked during message processing. + diff --git a/mail_hide_inline_attachments/readme/CONTRIBUTORS.rst b/mail_hide_inline_attachments/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..bb3a45bd7e --- /dev/null +++ b/mail_hide_inline_attachments/readme/CONTRIBUTORS.rst @@ -0,0 +1,6 @@ +Contributors +============ + +- `KMEE `_: + - Luis Felipe Miléo + diff --git a/mail_hide_inline_attachments/readme/DESCRIPTION.rst b/mail_hide_inline_attachments/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..b677677495 --- /dev/null +++ b/mail_hide_inline_attachments/readme/DESCRIPTION.rst @@ -0,0 +1,33 @@ +This module hides inline images from emails in the attachment list of records that +inherit from ``mail.thread`` and also from the ``attachment_ids`` field of the +``mail.message`` model. + +When an email contains embedded (inline) images, they are converted into attachments by +Odoo and become visible both in the email body and in the attachment list of the record +and message. This module filters these inline images so they appear only in the email +body. + +Features +-------- + +- Automatically detects attachments that are referenced inline in message bodies + through: + - CID (Content-ID) references in ```` tags + - ``data-filename`` attributes in ```` tags + - ``/web/image/{id}`` URLs in the ``src`` attribute of ```` tags +- Filters these attachments from the record's attachment list (via ``mail.thread``) +- Filters these attachments from the ``attachment_ids`` field of ``mail.message`` +- Images remain visible in the email body +- Works with all models that inherit from ``mail.thread`` + +How it works +------------ + +The module intercepts attachment processing at two points: + +1. **During message creation** (``mail.thread._message_post_process_attachments``): + Inline attachments are unlinked from the record (``res_model`` and ``res_id`` are + cleared), but remain linked to the message. +2. **During message formatting** (``mail.message._message_format``): Inline attachments + are filtered from the attachment list returned to the web client. + diff --git a/mail_hide_inline_attachments/readme/ROADMAP.rst b/mail_hide_inline_attachments/readme/ROADMAP.rst new file mode 100644 index 0000000000..5974ee11ab --- /dev/null +++ b/mail_hide_inline_attachments/readme/ROADMAP.rst @@ -0,0 +1,10 @@ +Roadmap +======= + +Planned future improvements: + +- Support for other types of inline content besides images +- Configuration option to disable inline attachment filtering +- Improvements in inline attachment detection for edge cases +- Support for inline attachments in threaded replies + diff --git a/mail_hide_inline_attachments/readme/USAGE.rst b/mail_hide_inline_attachments/readme/USAGE.rst new file mode 100644 index 0000000000..3ff5c70634 --- /dev/null +++ b/mail_hide_inline_attachments/readme/USAGE.rst @@ -0,0 +1,31 @@ +Usage +===== + +After installation, the module works automatically. No additional configuration is +required. + +Behavior +-------- + +When an email is received or sent with inline images: + +1. Images are processed normally and appear in the email body +2. Corresponding attachments are created in the system +3. Inline attachments are automatically filtered and do not appear: + - In the record's attachment list (chatter) + - In the ``attachment_ids`` field of ``mail.message`` +4. Only attachments that are not inline images remain visible in the attachment list + +Example +------- + +If an email contains: + +- 1 inline image (company logo) +- 2 normal attachments (PDF and DOCX) + +The result will be: + +- The inline image appears only in the email body +- The 2 normal attachments appear in the record's and message's attachment list + diff --git a/mail_hide_inline_attachments/static/description/index.html b/mail_hide_inline_attachments/static/description/index.html new file mode 100644 index 0000000000..98eab157b9 --- /dev/null +++ b/mail_hide_inline_attachments/static/description/index.html @@ -0,0 +1,522 @@ + + + + + +Mail - Hide Inline Attachments + + + +
+

Mail - Hide Inline Attachments

+ + +

Beta License: LGPL-3 OCA/social Translate me on Weblate Try me on Runboat

+

This module hides inline images from emails in the attachment list of records that +inherit from mail.thread and also from the attachment_ids field of the +mail.message model.

+

When an email contains embedded (inline) images, they are converted into attachments by +Odoo and become visible both in the email body and in the attachment list of the record +and message. This module filters these inline images so they appear only in the email +body.

+
+

Features

+
    +
  • Automatically detects attachments that are referenced inline in message bodies +through: +- CID (Content-ID) references in <img> tags +- data-filename attributes in <img> tags +- /web/image/{id} URLs in the src attribute of <img> tags
  • +
  • Filters these attachments from the record’s attachment list (via mail.thread)
  • +
  • Filters these attachments from the attachment_ids field of mail.message
  • +
  • Images remain visible in the email body
  • +
  • Works with all models that inherit from mail.thread
  • +
+
+
+

How it works

+

The module intercepts attachment processing at two points:

+
    +
  1. During message creation (mail.thread._message_post_process_attachments): +Inline attachments are unlinked from the record (res_model and res_id are +cleared), but remain linked to the message.
  2. +
  3. During message formatting (mail.message._message_format): Inline attachments +are filtered from the attachment list returned to the web client.
  4. +
+

Table of contents

+ + +
+

Configuration

+

This module does not require additional configuration. It works automatically after +installation.

+
+
+
+

Inline attachment detection

+

The module detects inline attachments through three methods:

+
    +
  1. CID (Content-ID): When an <img> tag has src="cid:xxx", the attachment with +that CID is considered inline
  2. +
  3. data-filename: When an <img> tag has the data-filename="filename" attribute, +the attachment with that name is considered inline
  4. +
  5. URL /web/image/{id}: When an <img> tag has src="/web/image/123", the +attachment with ID 123 is considered inline
  6. +
+

All these methods are automatically checked during message processing.

+
+

Usage

+
+
+

Usage

+

After installation, the module works automatically. No additional configuration is +required.

+
+
+
+

Behavior

+

When an email is received or sent with inline images:

+
    +
  1. Images are processed normally and appear in the email body
  2. +
  3. Corresponding attachments are created in the system
  4. +
  5. Inline attachments are automatically filtered and do not appear: +- In the record’s attachment list (chatter) +- In the attachment_ids field of mail.message
  6. +
  7. Only attachments that are not inline images remain visible in the attachment list
  8. +
+
+
+

Example

+

If an email contains:

+
    +
  • 1 inline image (company logo)
  • +
  • 2 normal attachments (PDF and DOCX)
  • +
+

The result will be:

+
    +
  • The inline image appears only in the email body
  • +
  • The 2 normal attachments appear in the record’s and message’s attachment list
  • +
+
+

Known issues / Roadmap

+
+
+

Roadmap

+

Planned future improvements:

+
    +
  • Support for other types of inline content besides images
  • +
  • Configuration option to disable inline attachment filtering
  • +
  • Improvements in inline attachment detection for edge cases
  • +
  • Support for inline attachments in threaded replies
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • KMEE
  • +
+
+
+

Contributors

+
+
+
+

Contributors

+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +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.

+

Current maintainer:

+

mileo

+

This module is part of the OCA/social project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/mail_hide_inline_attachments/tests/__init__.py b/mail_hide_inline_attachments/tests/__init__.py new file mode 100644 index 0000000000..59c3f5d7d9 --- /dev/null +++ b/mail_hide_inline_attachments/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2024 - KMEE +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html + +from . import test_mail_thread diff --git a/mail_hide_inline_attachments/tests/test_mail_thread.py b/mail_hide_inline_attachments/tests/test_mail_thread.py new file mode 100644 index 0000000000..a271a9cf51 --- /dev/null +++ b/mail_hide_inline_attachments/tests/test_mail_thread.py @@ -0,0 +1,191 @@ +# Copyright (C) 2024 - KMEE +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html + +import base64 + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged("post_install", "-at_install") +class TestMailThreadInlineAttachments(TransactionCase): + def setUp(self): + super().setUp() + self.partner = self.env["res.partner"].create( + { + "name": "Test Partner", + "email": "test@example.com", + } + ) + # Base64 encoded 1x1 PNG image + self.png_data_b64 = ( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwAD" + "hgGAWjR9awAAAABJRU5ErkJggg==" + ) + # Decoded PNG bytes for attachments parameter + self.png_data = base64.b64decode(self.png_data_b64) + # Base64 encoded minimal PDF + self.pdf_data_b64 = ( + "JVBERi0xLjQKJeLjz9MNCjEgMCBvYmoKPDwvVHlwZS9DYXRhbG9nL1BhZ2VzIDIgMCBSPj4K" + "ZW5kb2JqCjIgMCBvYmoKPDwvVHlwZS9QYWdlcz4+CmVuZG9iagp4cmVmCjAgMwowMDAwMDAw" + "MDAwIDY1NTM1IGYNCjAwMDAwMDAwMTUgMDAwMDAgbg0KMDAwMDAwMDA2MCAwMDAwMCBuDQp0" + "cmFpbGVyCjw8L1NpemUgMy9Sb290IDEgMCBSPj4Kc3RhcnR4cmVmCjEwOQolJUVPRg==" + ) + # Decoded PDF bytes for attachments parameter + self.pdf_data = base64.b64decode(self.pdf_data_b64) + + def test_inline_attachment_via_cid_is_hidden(self): + """Test that inline attachments referenced via CID are hidden""" + # Post message with inline attachment using CID + message = self.partner.message_post( + body="

Test message with inline image: " '

', + message_type="comment", + attachments=[("test_image.png", self.png_data, {"cid": "test_cid"})], + ) + + # Get the created attachment + attachment = message.attachment_ids.filtered( + lambda a: a.name == "test_image.png" + ) + self.assertTrue(attachment, "Attachment should be created") + + # Check that inline attachment is not linked to the record + attachments = self.partner._get_mail_thread_data_attachments() + self.assertNotIn( + attachment, + attachments, + "Inline attachment with CID should not appear in list", + ) + # Verify it's unlinked from the record + attachment.invalidate_recordset() + self.assertFalse(attachment.res_model) + self.assertFalse(attachment.res_id) + + def test_regular_attachments_are_shown(self): + """Test that regular (non-inline) attachments are still shown""" + # Post message with regular attachment (not referenced in body) + message = self.partner.message_post( + body="

Test message with attachment

", + message_type="comment", + attachments=[("test_document.pdf", self.pdf_data)], + ) + + # Get the created attachment + attachment = message.attachment_ids.filtered( + lambda a: a.name == "test_document.pdf" + ) + self.assertTrue(attachment, "Attachment should be created") + + # Check that regular attachment is linked to the record + attachments = self.partner._get_mail_thread_data_attachments() + self.assertIn( + attachment, + attachments, + "Regular attachment should appear in attachment list", + ) + # Verify it's linked to the record + self.assertEqual(attachment.res_model, "res.partner") + self.assertEqual(attachment.res_id, self.partner.id) + + def test_mixed_attachments(self): + """Test with both inline and regular attachments""" + # Post message with both inline (CID) and regular attachments + message = self.partner.message_post( + body='

Message with ' "and attachment

", + message_type="comment", + attachments=[ + ("inline_image.png", self.png_data, {"cid": "inline_cid"}), + ("document.pdf", self.pdf_data), + ], + ) + + inline_attachment = message.attachment_ids.filtered( + lambda a: a.name == "inline_image.png" + ) + regular_attachment = message.attachment_ids.filtered( + lambda a: a.name == "document.pdf" + ) + + self.assertTrue(inline_attachment, "Inline attachment should exist") + self.assertTrue(regular_attachment, "Regular attachment should exist") + + # Check results + attachments = self.partner._get_mail_thread_data_attachments() + self.assertNotIn( + inline_attachment, attachments, "Inline attachment should not appear" + ) + self.assertIn( + regular_attachment, attachments, "Regular attachment should appear" + ) + self.assertEqual(len(attachments), 1) + # Verify inline is unlinked, regular is linked + inline_attachment.invalidate_recordset() + regular_attachment.invalidate_recordset() + self.assertFalse(inline_attachment.res_model) + self.assertFalse(inline_attachment.res_id) + self.assertEqual(regular_attachment.res_model, "res.partner") + self.assertEqual(regular_attachment.res_id, self.partner.id) + + def test_inline_attachment_via_data_filename(self): + """Test that inline attachments referenced via data-filename are + hidden""" + # Post message with inline attachment using data-filename + message = self.partner.message_post( + body="

Test message with inline image: " + '

', + message_type="comment", + attachments=[("test_image.png", self.png_data)], + ) + + # Get the created attachment + attachment = message.attachment_ids.filtered( + lambda a: a.name == "test_image.png" + ) + self.assertTrue(attachment, "Attachment should be created") + + # Check that inline attachment is not linked to the record + attachments = self.partner._get_mail_thread_data_attachments() + self.assertNotIn( + attachment, + attachments, + "Inline attachment with data-filename should not appear", + ) + # Verify it's unlinked from the record + attachment.invalidate_recordset() + self.assertFalse(attachment.res_model) + self.assertFalse(attachment.res_id) + + def test_multiple_inline_attachments(self): + """Test that multiple inline attachments are all hidden""" + # Post message with multiple inline attachments using CID + message = self.partner.message_post( + body='

Message with and ' + '

', + message_type="comment", + attachments=[ + ("image1.png", self.png_data, {"cid": "cid1"}), + ("image2.png", self.png_data, {"cid": "cid2"}), + ("document.pdf", self.pdf_data), + ], + ) + + inline1 = message.attachment_ids.filtered(lambda a: a.name == "image1.png") + inline2 = message.attachment_ids.filtered(lambda a: a.name == "image2.png") + regular = message.attachment_ids.filtered(lambda a: a.name == "document.pdf") + + # Check results + attachments = self.partner._get_mail_thread_data_attachments() + self.assertNotIn(inline1, attachments) + self.assertNotIn(inline2, attachments) + self.assertIn(regular, attachments) + self.assertEqual(len(attachments), 1) + # Verify all inline are unlinked + inline1.invalidate_recordset() + inline2.invalidate_recordset() + self.assertFalse(inline1.res_model) + self.assertFalse(inline1.res_id) + self.assertFalse(inline2.res_model) + self.assertFalse(inline2.res_id) + self.assertEqual(regular.res_model, "res.partner") + self.assertEqual(regular.res_id, self.partner.id) diff --git a/setup/mail_hide_inline_attachments/odoo/addons/mail_hide_inline_attachments b/setup/mail_hide_inline_attachments/odoo/addons/mail_hide_inline_attachments new file mode 120000 index 0000000000..e6f03afbd8 --- /dev/null +++ b/setup/mail_hide_inline_attachments/odoo/addons/mail_hide_inline_attachments @@ -0,0 +1 @@ +../../../../mail_hide_inline_attachments \ No newline at end of file diff --git a/setup/mail_hide_inline_attachments/setup.py b/setup/mail_hide_inline_attachments/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/mail_hide_inline_attachments/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)