From 48661d83d723dad02c5cb9af00d99b596bf3a6e6 Mon Sep 17 00:00:00 2001 From: PrabhuAppalapuriSAMSA-IT Date: Wed, 19 Nov 2025 17:35:54 +0100 Subject: [PATCH 1/3] 17.0 - Mail Drop Target --- mail_drop_target/README.rst | 106 ++++ mail_drop_target/__init__.py | 4 + mail_drop_target/__manifest__.py | 22 + mail_drop_target/controllers/__init__.py | 3 + mail_drop_target/controllers/discuss.py | 45 ++ mail_drop_target/i18n/it.po | 94 ++++ mail_drop_target/models/__init__.py | 5 + mail_drop_target/models/ir_attachment.py | 50 ++ mail_drop_target/models/mail_thread.py | 157 ++++++ .../models/res_config_settings.py | 17 + mail_drop_target/readme/CONFIGURATION.rst | 3 + mail_drop_target/readme/CONTRIBUTORS.rst | 6 + mail_drop_target/readme/CREDITS.rst | 2 + mail_drop_target/readme/DESCRIPTION.rst | 7 + mail_drop_target/readme/ROADMAP.rst | 2 + mail_drop_target/readme/USAGE.rst | 4 + mail_drop_target/static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 452 ++++++++++++++++++ .../src/core/common/thread_service_patch.js | 38 ++ .../src/js/attachment_upload_service.js | 126 +++++ mail_drop_target/tests/__init__.py | 1 + mail_drop_target/tests/sample.eml | 75 +++ mail_drop_target/tests/sample.msg | Bin 0 -> 16896 bytes .../tests/sample_include_attachment.msg | Bin 0 -> 73216 bytes .../tests/test_mail_drop_target.py | 100 ++++ .../views/res_config_settings_views.xml | 15 + 26 files changed, 1334 insertions(+) create mode 100644 mail_drop_target/README.rst create mode 100644 mail_drop_target/__init__.py create mode 100644 mail_drop_target/__manifest__.py create mode 100644 mail_drop_target/controllers/__init__.py create mode 100644 mail_drop_target/controllers/discuss.py create mode 100644 mail_drop_target/i18n/it.po create mode 100644 mail_drop_target/models/__init__.py create mode 100644 mail_drop_target/models/ir_attachment.py create mode 100644 mail_drop_target/models/mail_thread.py create mode 100644 mail_drop_target/models/res_config_settings.py create mode 100644 mail_drop_target/readme/CONFIGURATION.rst create mode 100644 mail_drop_target/readme/CONTRIBUTORS.rst create mode 100644 mail_drop_target/readme/CREDITS.rst create mode 100644 mail_drop_target/readme/DESCRIPTION.rst create mode 100644 mail_drop_target/readme/ROADMAP.rst create mode 100644 mail_drop_target/readme/USAGE.rst create mode 100644 mail_drop_target/static/description/icon.png create mode 100644 mail_drop_target/static/description/index.html create mode 100644 mail_drop_target/static/src/core/common/thread_service_patch.js create mode 100644 mail_drop_target/static/src/js/attachment_upload_service.js create mode 100644 mail_drop_target/tests/__init__.py create mode 100644 mail_drop_target/tests/sample.eml create mode 100644 mail_drop_target/tests/sample.msg create mode 100644 mail_drop_target/tests/sample_include_attachment.msg create mode 100644 mail_drop_target/tests/test_mail_drop_target.py create mode 100644 mail_drop_target/views/res_config_settings_views.xml diff --git a/mail_drop_target/README.rst b/mail_drop_target/README.rst new file mode 100644 index 000000000..1d0bdec97 --- /dev/null +++ b/mail_drop_target/README.rst @@ -0,0 +1,106 @@ +========================== +Drag & drop emails to Odoo +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e919dcef6dab8ff8aaa3381bae42c978fa61e3c9bbc7a8c5cca62e850cfb2525 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-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_drop_target + :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_drop_target + :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 was written to allow users to drag&drop emails from their desktop to Odoo. + +It supports as well RFC822 .eml files as Outlook .msg (those only if `an extra library `_ is installed) files. + +When the mail is dropped to an odoo record, it will automatically send a notification +of that new message that has been added to all the existing followers. It is possible +to disable this notification. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module, you need to: + +#. save your emails on the desktop / somewhere in the file system +#. drag them to your browser, and drop them on the chatter of the record you want to attach your email to + +Known issues / Roadmap +====================== + +* most mail clients won't allow you to drag mails directly from the mail client, you'll need some plugin for that +* for corporate environments, it might be feasible to support imap URLs and get the mail in question on the server side + +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 +~~~~~~~ + +* Therp BV + +Contributors +~~~~~~~~~~~~ + +* Prabhu Appalapuri +* Artiom Kichojal +* Holger Brunn +* Enric Tobella +* Lois Rilo +* Nguyen Minh Chien + +Other credits +~~~~~~~~~~~~~ +*The migration of this module from 16.0 to 17.0 was financially supported by SAMSA-IT +*The migration of this module from 15.0 to 16.0 was financially supported by Camptocamp + +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. + +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_drop_target/__init__.py b/mail_drop_target/__init__.py new file mode 100644 index 000000000..9484ea350 --- /dev/null +++ b/mail_drop_target/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import controllers +from . import models diff --git a/mail_drop_target/__manifest__.py b/mail_drop_target/__manifest__.py new file mode 100644 index 000000000..15518b91e --- /dev/null +++ b/mail_drop_target/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Drag & drop emails to Odoo", + "version": "17.0.1.0.0", + "author": "Therp BV,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Discuss", + "website": "https://github.com/OCA/social", + "summary": "Attach emails to Odoo by dragging them from your desktop", + "depends": ["mail"], + "external_dependencies": {"python": ["extract_msg", "cryptography<37"]}, + "data": [ + "views/res_config_settings_views.xml" + ], + "assets": { + "web.assets_backend": [ + "mail_drop_target/static/src/core/common/thread_service_patch.js", + "mail_drop_target/static/src/js/attachment_upload_service.js" + ], + }, +} diff --git a/mail_drop_target/controllers/__init__.py b/mail_drop_target/controllers/__init__.py new file mode 100644 index 000000000..636ed8add --- /dev/null +++ b/mail_drop_target/controllers/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import discuss diff --git a/mail_drop_target/controllers/discuss.py b/mail_drop_target/controllers/discuss.py new file mode 100644 index 000000000..048e37d7d --- /dev/null +++ b/mail_drop_target/controllers/discuss.py @@ -0,0 +1,45 @@ +# Copyright Nguyen Minh Chien (chien@trobz.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import http +from odoo.exceptions import AccessError, UserError +from odoo.http import request +from odoo.tools.translate import _ + +from odoo.addons.mail.controllers.attachment import AttachmentController + + +class DiscussControllerInherit(AttachmentController): + @http.route("/mail/attachment/upload", methods=["POST"], type="http", auth="public") + def mail_attachment_upload(self, ufile, thread_id, thread_model, is_pending=False, **kwargs): + if not is_pending or is_pending == "false": + # Add this point, make sure the message related to the uploaded + # file does exist. + resp = self.mail_attachment_upload_email( + ufile, thread_id, thread_model) + if resp: + return resp + + return super().mail_attachment_upload(ufile, thread_id, thread_model, is_pending, **kwargs) + + def mail_attachment_upload_email(self, ufile, thread_id, thread_model): + channel_member = request.env["discuss.channel.member"] + if thread_model == "mail.channel": + channel_member = request.env["discuss.channel.member"]._get_as_sudo_from_request_or_raise( + request=request, channel_id=int(thread_id)) + + try: + mail_resp = channel_member.env["ir.attachment"].read_mail_file_content( + ufile.filename, ufile.read(), int(thread_id), thread_model + ) + ufile.seek(0) + if not mail_resp: + return False + responseData = {"email_upload": 1} + except AccessError: + responseData = { + "error": _("You are not allowed to upload an attachment here.") + } + except UserError as err: + responseData = {"error": str(err)} + return request.make_json_response(responseData) diff --git a/mail_drop_target/i18n/it.po b/mail_drop_target/i18n/it.po new file mode 100644 index 000000000..03ffdc1f0 --- /dev/null +++ b/mail_drop_target/i18n/it.po @@ -0,0 +1,94 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_drop_target +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-10-04 09:42+0000\n" +"PO-Revision-Date: 2024-10-04 09:42+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: mail_drop_target +#: model:ir.model,name:mail_drop_target.model_ir_attachment +msgid "Attachment" +msgstr "Allegato" + +#. module: mail_drop_target +#: model:ir.model,name:mail_drop_target.model_res_config_settings +msgid "Config Settings" +msgstr "Impostazioni di configurazione" + +#. module: mail_drop_target +#: model_terms:ir.ui.view,arch_db:mail_drop_target.res_config_settings_view_form_mail_drop_target +msgid "Disable Mail Drag&Drop Notification" +msgstr "Disabilita notifica e-mail trascina&rilascia" + +#. module: mail_drop_target +#: model:ir.model.fields,field_description:mail_drop_target.field_res_config_settings__disable_notify_mail_drop_target +msgid "Disable Notification followers on mail dropped to a Thread" +msgstr "Disabilita notifica chi segue per e-mail inserita in una discussione" + +#. module: mail_drop_target +#: model:ir.model,name:mail_drop_target.model_mail_thread +msgid "Email Thread" +msgstr "Discussione e-mail" + +#. module: mail_drop_target +#. odoo-javascript +#: code:addons/mail_drop_target/static/src/js/attachment_upload_service.js:0 +#, python-format +msgid "File too large" +msgstr "" + +#. module: mail_drop_target +#. odoo-python +#: code:addons/mail_drop_target/models/mail_thread.py:0 +#, python-format +msgid "Install the msg-extractor library to handle .msg files" +msgstr "Installare la libreria msg-extractor per festire i file .msg" + +#. module: mail_drop_target +#. odoo-javascript +#: code:addons/mail_drop_target/static/src/js/attachment_upload_service.js:0 +#, python-format +msgid "Server error" +msgstr "" + +#. module: mail_drop_target +#. odoo-python +#: code:addons/mail_drop_target/models/mail_thread.py:0 +#, python-format +msgid "This message is already imported." +msgstr "Questo messaggio è già stato importato." + +#. module: mail_drop_target +#: model_terms:ir.ui.view,arch_db:mail_drop_target.res_config_settings_view_form_mail_drop_target +msgid "" +"When a user drops an email into an existing thread the followers of the " +"thread will not be notified." +msgstr "" + +#. module: mail_drop_target +#: model:ir.model.fields,help:mail_drop_target.field_res_config_settings__disable_notify_mail_drop_target +msgid "" +"When this setting is set, when a user drops an email into an existing thread" +" the followers of the thread will not be notified. This sets an " +"ir.config.parameter mail_drop_target.disable_notify" +msgstr "" +"Quanto è attiva questa impostazione, quando un utente rilascia una e.mail in" +" una discussione esistente, chi segue la discussione non verrà avvisato. " +"Questo imposta un ir.config.parameter in mail_drop_target.disable_notify" + +#. module: mail_drop_target +#. odoo-python +#: code:addons/mail_drop_target/controllers/discuss.py:0 +#, python-format +msgid "You are not allowed to upload an attachment here." +msgstr "Non si ha l'autorizzazione per caricare qui un allegato." \ No newline at end of file diff --git a/mail_drop_target/models/__init__.py b/mail_drop_target/models/__init__.py new file mode 100644 index 000000000..98fd6d1ef --- /dev/null +++ b/mail_drop_target/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import ir_attachment +from . import mail_thread +from . import res_config_settings diff --git a/mail_drop_target/models/ir_attachment.py b/mail_drop_target/models/ir_attachment.py new file mode 100644 index 000000000..70f2c8563 --- /dev/null +++ b/mail_drop_target/models/ir_attachment.py @@ -0,0 +1,50 @@ +# Copyright Nguyen Minh Chien (chien@trobz.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import base64 +import os + +from odoo import api, models + + +class IrAttachment(models.Model): + _inherit = "ir.attachment" + + def _get_email_file_extensions(self): + return ["msg", "eml"] + + def _process_email_file_msg(self, res_obj, raw_content): + if not hasattr(res_obj, "message_process_msg"): + return False + message = base64.b64encode(raw_content) + thread_id = res_obj.message_process_msg( + res_obj._name, message, thread_id=res_obj.id + ) + return thread_id + + @api.model + def _process_email_file_default(self, res_obj, raw_content): + if not hasattr(res_obj, "message_drop"): + return False + message = raw_content + thread_id = res_obj.message_drop( + res_obj._name, message, thread_id=res_obj.id) + return thread_id + + def read_mail_file_content(self, file_name, raw_content, res_id, res_model): + file_extensions = self._get_email_file_extensions() + name_lst = os.path.splitext(file_name) + file_extension = name_lst[-1].lower().replace(".", "") + if not file_extension or file_extension not in file_extensions: + return False + + res_obj = self.env[res_model].browse(res_id) + if not res_obj: + return False + + handler = "_process_email_file_{}".format(file_extension) + if not hasattr(self, handler): + handler = "_process_email_file_default" + + res = getattr(self, handler)(res_obj, raw_content) + return res diff --git a/mail_drop_target/models/mail_thread.py b/mail_drop_target/models/mail_thread.py new file mode 100644 index 000000000..288ee2d9d --- /dev/null +++ b/mail_drop_target/models/mail_thread.py @@ -0,0 +1,157 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from base64 import b64decode + +import lxml.html + +from odoo import _, api, exceptions, models +from odoo.tools import pycompat, ustr + +try: + from extract_msg import Message +except ImportError: + Message = None + + +class MailThread(models.AbstractModel): + _inherit = "mail.thread" + + @api.model + def message_drop( + self, + model, + message, + custom_values=None, + save_original=False, + strip_attachments=False, + thread_id=None, + ): + disable_notify_mail_drop_target = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("mail_drop_target.disable_notify", default=False) + ) + self_message_process = self + if disable_notify_mail_drop_target: + self_message_process = self_message_process.with_context( + message_create_from_mail_mail=True + ) + result = self_message_process.sudo().message_process( + model, + message, + custom_values=custom_values, + save_original=save_original, + strip_attachments=strip_attachments, + thread_id=thread_id, + ) + if not result: + return self.message_drop_existing( + model, + message, + custom_values=custom_values, + save_original=save_original, + strip_attachments=strip_attachments, + thread_id=thread_id, + ) + return result + + @api.model + def message_drop_existing( + self, + model, + message, + custom_values=None, + save_original=False, + strip_attachments=False, + thread_id=None, + ): + message = _("This message is already imported.") + raise exceptions.UserError(message) + + @api.model + def message_process_msg( + self, + model, + message, + custom_values=None, + save_original=False, + strip_attachments=False, + thread_id=None, + ): + """Convert message to RFC2822 and pass to message_process""" + if not Message: + raise exceptions.UserError( + _("Install the msg-extractor library to handle .msg files") + ) + message_msg = Message(b64decode(message)) + try: + message_id = message_msg.messageId + except AttributeError: + # Using extract_msg < 0.24.4 + message_id = message_msg.message_id + msg_body = message_msg.htmlBody or message_msg.body + subtype = ( + lxml.html.fromstring(msg_body).find(".//*") is not None + and "html" + or "plain" + ) + + message_email = self.env["ir.mail_server"].build_email( + message_msg.sender, + message_msg.to.split(","), + message_msg.subject, + # prefer html bodies to text + msg_body, + email_cc=message_msg.cc, + message_id=message_id.strip(), + attachments=[ + (attachment.longFilename, attachment.data, attachment.mimetype) + for attachment in message_msg.attachments + ], + subtype=subtype, + ) + # We need to override message date, as an error rises when processing it + # directly with headers + key = pycompat.to_text(ustr("date")) + del message_email[key] + message_email[key] = message_msg.date + return self.message_drop( + model, + message_email.as_string(), + custom_values=custom_values, + save_original=save_original, + strip_attachments=strip_attachments, + thread_id=thread_id, + ) + + def _notify_thread_by_email( + self, + message, + recipients_data, + msg_vals=False, + mail_auto_delete=True, # mail.mail + model_description=False, + force_email_company=False, + force_email_lang=False, # rendering + resend_existing=False, + force_send=True, + send_after_commit=True, # email send + subtitles=None, + **kwargs + ): + if self.env.context.get("message_create_from_mail_mail", False): + return + return super()._notify_thread_by_email( + message, + recipients_data, + msg_vals=msg_vals, + mail_auto_delete=mail_auto_delete, + model_description=model_description, + force_email_company=force_email_company, + force_email_lang=force_email_lang, + resend_existing=resend_existing, + force_send=force_send, + send_after_commit=send_after_commit, + subtitles=subtitles, + **kwargs, + ) diff --git a/mail_drop_target/models/res_config_settings.py b/mail_drop_target/models/res_config_settings.py new file mode 100644 index 000000000..6122fb002 --- /dev/null +++ b/mail_drop_target/models/res_config_settings.py @@ -0,0 +1,17 @@ +# Copyright 2019-20 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + disable_notify_mail_drop_target = fields.Boolean( + "Disable Notification followers on mail dropped to a Thread", + config_parameter="mail_drop_target.disable_notify", + help="When this setting is set, when a user drops an " + "email into an existing thread the followers of the thread will " + "not be notified. This sets an ir.config.parameter " + "mail_drop_target.disable_notify", + ) diff --git a/mail_drop_target/readme/CONFIGURATION.rst b/mail_drop_target/readme/CONFIGURATION.rst new file mode 100644 index 000000000..79e2eb7b3 --- /dev/null +++ b/mail_drop_target/readme/CONFIGURATION.rst @@ -0,0 +1,3 @@ +To disable the automatic notification to existing followers when you drop an +email to a record, go to *Settings*, activate the developer mode, and then go to +*Settings / General Settings* and set the flag 'Disable Mail Drag&Drop Notification'. diff --git a/mail_drop_target/readme/CONTRIBUTORS.rst b/mail_drop_target/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..b1f41cc2e --- /dev/null +++ b/mail_drop_target/readme/CONTRIBUTORS.rst @@ -0,0 +1,6 @@ +* Holger Brunn +* Enric Tobella +* Lois Rilo +* Nguyen Minh Chien +* Prabhu Appalapuri +* Artiom Kichojal diff --git a/mail_drop_target/readme/CREDITS.rst b/mail_drop_target/readme/CREDITS.rst new file mode 100644 index 000000000..cb98d23fa --- /dev/null +++ b/mail_drop_target/readme/CREDITS.rst @@ -0,0 +1,2 @@ +*The migration of this module from 16.0 to 17.0 was financially supported by SAMSA-IT +*The migration of this module from 15.0 to 16.0 was financially supported by Camptocamp diff --git a/mail_drop_target/readme/DESCRIPTION.rst b/mail_drop_target/readme/DESCRIPTION.rst new file mode 100644 index 000000000..c017ccdff --- /dev/null +++ b/mail_drop_target/readme/DESCRIPTION.rst @@ -0,0 +1,7 @@ +This module was written to allow users to drag&drop emails from their desktop to Odoo. + +It supports as well RFC822 .eml files as Outlook .msg (those only if `an extra library `_ is installed) files. + +When the mail is dropped to an odoo record, it will automatically send a notification +of that new message that has been added to all the existing followers. It is possible +to disable this notification. diff --git a/mail_drop_target/readme/ROADMAP.rst b/mail_drop_target/readme/ROADMAP.rst new file mode 100644 index 000000000..6e2dc0854 --- /dev/null +++ b/mail_drop_target/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +* most mail clients won't allow you to drag mails directly from the mail client, you'll need some plugin for that +* for corporate environments, it might be feasible to support imap URLs and get the mail in question on the server side diff --git a/mail_drop_target/readme/USAGE.rst b/mail_drop_target/readme/USAGE.rst new file mode 100644 index 000000000..080e2b51f --- /dev/null +++ b/mail_drop_target/readme/USAGE.rst @@ -0,0 +1,4 @@ +To use this module, you need to: + +#. save your emails on the desktop / somewhere in the file system +#. drag them to your browser, and drop them on the chatter of the record you want to attach your email to diff --git a/mail_drop_target/static/description/icon.png b/mail_drop_target/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/mail_drop_target/static/description/index.html b/mail_drop_target/static/description/index.html new file mode 100644 index 000000000..012cf278d --- /dev/null +++ b/mail_drop_target/static/description/index.html @@ -0,0 +1,452 @@ + + + + + +Drag & drop emails to Odoo + + + +
+

Drag & drop emails to Odoo

+ + +

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

+

This module was written to allow users to drag&drop emails from their desktop to Odoo.

+

It supports as well RFC822 .eml files as Outlook .msg (those only if an extra library is installed) files.

+

When the mail is dropped to an odoo record, it will automatically send a notification +of that new message that has been added to all the existing followers. It is possible +to disable this notification.

+

Table of contents

+ +
+

Usage

+

To use this module, you need to:

+
    +
  1. save your emails on the desktop / somewhere in the file system
  2. +
  3. drag them to your browser, and drop them on the chatter of the record you want to attach your email to
  4. +
+
+
+

Known issues / Roadmap

+
    +
  • most mail clients won’t allow you to drag mails directly from the mail client, you’ll need some plugin for that
  • +
  • for corporate environments, it might be feasible to support imap URLs and get the mail in question on the server side
  • +
+
+
+

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

+
    +
  • Therp BV
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The migration of this module from 16.0 to 17.0 was financially supported by SAMSA-IT

+

The migration of this module from 15.0 to 16.0 was financially supported by Camptocamp

+
+
+

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.

+

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_drop_target/static/src/core/common/thread_service_patch.js b/mail_drop_target/static/src/core/common/thread_service_patch.js new file mode 100644 index 000000000..ae8c9a498 --- /dev/null +++ b/mail_drop_target/static/src/core/common/thread_service_patch.js @@ -0,0 +1,38 @@ +/** @odoo-module */ + +import { ThreadService } from "@mail/core/common/thread_service"; +import { patch } from "@web/core/utils/patch"; +import { sortBy } from "@web/core/utils/arrays"; +const FETCH_LIMIT = 30; + + +patch(ThreadService.prototype, { + /** + * @param {import("models").Thread} thread + * @param {{after: Number, before: Number}} + */ + async fetchMessages(thread, { after, before } = {}) { + thread.status = "loading"; + if (thread.type === "chatter" && !thread.id) { + thread.isLoaded = true; + return []; + } + try { + // ordered messages received: newest to oldest + const { messages: rawMessages } = await this.rpc(this.getFetchRoute(thread), { + ...this.getFetchParams(thread), + limit: FETCH_LIMIT, + after, + before, + }); + const messages = this.store.Message.insert(sortBy(rawMessages, 'date').reverse().reverse(), { html: true }); + thread.isLoaded = true; + return messages; + } catch (e) { + thread.hasLoadingFailed = true; + throw e; + } finally { + thread.status = "ready"; + } + } +}); diff --git a/mail_drop_target/static/src/js/attachment_upload_service.js b/mail_drop_target/static/src/js/attachment_upload_service.js new file mode 100644 index 000000000..e42118d8c --- /dev/null +++ b/mail_drop_target/static/src/js/attachment_upload_service.js @@ -0,0 +1,126 @@ +/* @odoo-module */ + +import { AttachmentUploadService } from "@mail/core/common/attachment_upload_service"; + +import { patch } from "@web/core/utils/patch"; +import { url } from "@web/core/utils/urls"; +import { _t } from "@web/core/l10n/translation"; +import { Deferred } from "@web/core/utils/concurrency"; + +patch(AttachmentUploadService.prototype, { + setup(env, services) { + this.env = env; + this.fileUploadService = services["file_upload"]; + /** @type {import("@mail/core/common/store_service").Store} */ + this.store = services["mail.store"]; + this.notificationService = services["notification"]; + /** @type {import("@mail/core/common/attachment_service").AttachmentService} */ + this.attachmentService = services["mail.attachment"]; + + this.abortByAttachmentId = new Map(); + this.deferredByAttachmentId = new Map(); + this.uploadingAttachmentIds = new Set(); + this.hookersByTmpId = new Map(); + + this.fileUploadService.bus.addEventListener( + "FILE_UPLOAD_ADDED", + ({ detail: { upload } }) => { + const tmpId = parseInt(upload.data.get("temporary_id")); + if (!this.uploadingAttachmentIds.has(tmpId)) { + return; + } + const hooker = this.hookersByTmpId.get(tmpId); + const threadId = parseInt(upload.data.get("thread_id")); + const threadModel = upload.data.get("thread_model"); + const tmpUrl = upload.data.get("tmp_url"); + const originThread = this.store.Thread.insert({ + model: threadModel, + id: threadId, + }); + this.abortByAttachmentId.set(tmpId, upload.xhr.abort.bind(upload.xhr)); + const attachment = this.store.Attachment.insert( + this._makeAttachmentData( + upload, + tmpId, + hooker.composer ? undefined : originThread, + tmpUrl + ) + ); + if (hooker.composer) { + hooker.composer.attachments.push(attachment); + } + } + ); + this.fileUploadService.bus.addEventListener( + "FILE_UPLOAD_LOADED", + ({ detail: { upload } }) => { + const tmpId = parseInt(upload.data.get("temporary_id")); + if (!this.uploadingAttachmentIds.has(tmpId)) { + return; + } + + this.uploadingAttachmentIds.delete(tmpId); + this.abortByAttachmentId.delete(tmpId); + const hooker = this.hookersByTmpId.get(tmpId); + if (upload.xhr.status === 413) { + this.notificationService.add(_t("File too large"), { type: "danger" }); + this.hookersByTmpId.delete(tmpId); + return; + } + if (upload.xhr.status !== 200) { + this.notificationService.add(_t("Server error"), { type: "danger" }); + this.hookersByTmpId.delete(tmpId); + return; + } + const response = JSON.parse(upload.xhr.response); + if (response.error) { + this.notificationService.add(response.error, { type: "danger" }); + this.hookersByTmpId.delete(tmpId); + return; + } + let xhrResp = JSON.parse(upload.xhr.response); + if(xhrResp.email_upload){ + location.reload(); + return + } + const threadId = parseInt(upload.data.get("thread_id")); + const threadModel = upload.data.get("thread_model"); + const originThread = this.store.Thread.get({ model: threadModel, id: threadId }); + const attachment = this.store.Attachment.insert({ + ...response, + extension: upload.title.split(".").pop(), + originThread: hooker.composer ? undefined : originThread, + }); + if (hooker.composer) { + const index = hooker.composer.attachments.findIndex(({ id }) => id === tmpId); + if (index >= 0) { + hooker.composer.attachments[index] = attachment; + } else { + hooker.composer.attachments.push(attachment); + } + } + const def = this.deferredByAttachmentId.get(tmpId); + this.unlink(this.store.Attachment.get(tmpId)); + if (def) { + def.resolve(attachment); + this.deferredByAttachmentId.delete(tmpId); + } + hooker.onFileUploaded?.(); + this.hookersByTmpId.delete(tmpId); + } + ); + this.fileUploadService.bus.addEventListener( + "FILE_UPLOAD_ERROR", + ({ detail: { upload } }) => { + const tmpId = parseInt(upload.data.get("temporary_id")); + if (!this.uploadingAttachmentIds.has(tmpId)) { + return; + } + this.abortByAttachmentId.delete(tmpId); + this.deferredByAttachmentId.delete(tmpId); + this.uploadingAttachmentIds.delete(parseInt(tmpId)); + this.hookersByTmpId.delete(tmpId); + } + ); + } +}); diff --git a/mail_drop_target/tests/__init__.py b/mail_drop_target/tests/__init__.py new file mode 100644 index 000000000..1dd00d054 --- /dev/null +++ b/mail_drop_target/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mail_drop_target diff --git a/mail_drop_target/tests/sample.eml b/mail_drop_target/tests/sample.eml new file mode 100644 index 000000000..cd2cfa6ef --- /dev/null +++ b/mail_drop_target/tests/sample.eml @@ -0,0 +1,75 @@ +X-MDAV-Result: clean +X-MDAV-Processed: mail3.creublanca.es, Wed, 26 Jun 2019 18:08:19 +0200 +Return-path: +Authentication-Results: mail3.creublanca.es; + auth=pass (login) smtp.auth=etobella@creublanca.es +Received: from WorldClient by creublanca.es with ESMTPA id md50020655392.msg; + Wed, 26 Jun 2019 18:08:19 +0200 +X-Spam-Processed: mail3.creublanca.es, Wed, 26 Jun 2019 18:08:19 +0200 + (not processed: message from trusted or authenticated source) +X-MDArrival-Date: Wed, 26 Jun 2019 18:08:19 +0200 +X-Authenticated-Sender: etobella@creublanca.es +X-Rcpt-To: etobella@creublanca.es +X-MDRcpt-To: etobella@creublanca.es +X-Return-Path: prvs=10800b8cdb=etobella@creublanca.es +X-Envelope-From: etobella@creublanca.es +X-MDaemon-Deliver-To: etobella@creublanca.es +Received: by creublanca.es via MDaemon Webmail with HTTP; + Wed, 26 Jun 2019 18:08:16 +0200 +Date: Wed, 26 Jun 2019 18:08:16 +0200 +From: "Enric Tobella" +To: "Enric Tobella" +Subject: Test +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="0626-1608-16-03-PART_BREAK" +Message-ID: +X-Mailer: MDaemon Webmail 19.0.2 + +--0626-1608-16-03-PART_BREAK +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: 8bit + +TEST + + + + +Tanto este mensaje como los documentos que, en su caso, lleve como anexos, +pueden contener informacin reservada y/o confidencial, destinada exclusivamente +para el uso del destinatario o la persona responsable de entregarlo al mismo, +estando su uso no autorizado prohibido legalmente. +Su contenido no constituye un compromiso para Creu Blanca (la empresa remitente) +salvo ratificacin escrita por ambas partes. En caso de su recepcin por error, +rogamos nos lo comunique por igual va, se abstenga de realizar copias del mensaje +o documentos adjuntos, remitirlo o facilitarlo a un tercero, y proceda en su defecto, +a su eliminacin. + +--0626-1608-16-03-PART_BREAK +Content-Type: text/html; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + + +
TEST
+

+
+
+Tanto este mensaje como los documentos que, en su caso, lleve como anexos, = +
+pueden contener informaci=F3n reservada y/o confidencial, destinada exclusi= +vamente
+para el uso del destinatario o la persona responsable de entregarlo al mism= +o,
+estando su uso no autorizado prohibido legalmente.
+Su contenido no constituye un compromiso para Creu Blanca (la empresa remit= +ente)
+salvo ratificaci=F3n escrita por ambas partes. En caso de su recepci=F3n po= +r error,
+rogamos nos lo comunique por igual v=EDa, se abstenga de realizar copias de= +l mensaje
+o documentos adjuntos, remitirlo o facilitarlo a un tercero, y proceda en s= +u defecto,
+a su eliminaci=F3n.
+ + +--0626-1608-16-03-PART_BREAK-- diff --git a/mail_drop_target/tests/sample.msg b/mail_drop_target/tests/sample.msg new file mode 100644 index 0000000000000000000000000000000000000000..0a5ffcd8203e43ab258112d401a89fd138b3d34f GIT binary patch literal 16896 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 literal 0 HcmV?d00001 diff --git a/mail_drop_target/tests/sample_include_attachment.msg b/mail_drop_target/tests/sample_include_attachment.msg new file mode 100644 index 0000000000000000000000000000000000000000..9f46bed57b3bd870e1de1dbd45d382f5e06fc1e0 GIT binary patch literal 73216 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 literal 0 HcmV?d00001 diff --git a/mail_drop_target/tests/test_mail_drop_target.py b/mail_drop_target/tests/test_mail_drop_target.py new file mode 100644 index 000000000..9945c4a57 --- /dev/null +++ b/mail_drop_target/tests/test_mail_drop_target.py @@ -0,0 +1,100 @@ +import base64 +from unittest.mock import patch + +from odoo import exceptions, tools +from odoo.tests.common import TransactionCase + + +class TestMailDropTarget(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.partner = cls.env["res.partner"].create({"name": "TEST PARTNER"}) + cls.partner.message_subscribe(partner_ids=cls.partner.ids) + + def test_eml(self): + message = tools.file_open("addons/mail_drop_target/tests/sample.eml").read() + comments = len(self.partner.message_ids) + self.partner.message_process( + self.partner._name, message, thread_id=self.partner.id + ) + self.partner.invalidate_recordset() + self.assertEqual(comments + 1, len(self.partner.message_ids)) + with self.assertRaises(exceptions.UserError): + self.partner.message_drop( + self.partner._name, message, thread_id=self.partner.id + ) + + def test_msg(self): + message = base64.b64encode( + tools.file_open( + "addons/mail_drop_target/tests/sample.msg", mode="rb" + ).read() + ) + comments = len(self.partner.message_ids) + self.partner.message_process_msg( + self.partner._name, message, thread_id=self.partner.id + ) + self.partner.invalidate_recordset() + self.assertEqual(comments + 1, len(self.partner.message_ids)) + msg = self.partner.message_ids.filtered(lambda m: m.subject == "Test") + self.assertIsNotNone(msg.notified_partner_ids) + with self.assertRaises(exceptions.UserError): + self.partner.message_process_msg( + self.partner._name, message, thread_id=self.partner.id + ) + + def test_msg_with_attachment(self): + message = base64.b64encode( + tools.file_open( + "addons/mail_drop_target/tests/sample_include_attachment.msg", mode="rb" + ).read() + ) + comments = len(self.partner.message_ids) + self.partner.message_process_msg( + self.partner._name, message, thread_id=self.partner.id + ) + self.partner.invalidate_recordset() + self.assertEqual(comments + 1, len(self.partner.message_ids)) + msg = self.partner.message_ids.filtered( + lambda m: m.subject == "Test Mail Attachment" + ) + self.assertIsNotNone(msg.notified_partner_ids) + with self.assertRaises(exceptions.UserError): + self.partner.message_process_msg( + self.partner._name, message, thread_id=self.partner.id + ) + + def test_no_msgextract(self): + with self.assertRaises(exceptions.UserError), patch( + "odoo.addons.mail_drop_target.models.mail_thread.Message", new=False + ): + self.test_msg() + + with self.assertRaises(exceptions.UserError), patch( + "odoo.addons.mail_drop_target.models.mail_thread.Message", new=False + ): + self.test_msg_with_attachment() + + def test_msg_no_notification(self): + message = base64.b64encode( + tools.file_open( + "addons/mail_drop_target/tests/sample.msg", mode="rb" + ).read() + ) + settings = self.env["res.config.settings"].create({}) + settings.disable_notify_mail_drop_target = True + settings.execute() + comments = len(self.partner.message_ids) + self.partner.message_process_msg( + self.partner._name, message, thread_id=self.partner.id + ) + self.partner.invalidate_recordset() + self.assertEqual(comments + 1, len(self.partner.message_ids)) + msg = self.partner.message_ids.filtered(lambda m: m.subject == "Test") + self.assertEqual(len(msg.notified_partner_ids), 0) + with self.assertRaises(exceptions.UserError): + self.partner.message_process_msg( + self.partner._name, message, thread_id=self.partner.id + ) diff --git a/mail_drop_target/views/res_config_settings_views.xml b/mail_drop_target/views/res_config_settings_views.xml new file mode 100644 index 000000000..1563ef7fb --- /dev/null +++ b/mail_drop_target/views/res_config_settings_views.xml @@ -0,0 +1,15 @@ + + + + res.config.settings.view.form.inherit.mail + res.config.settings + + + + + + + + + + \ No newline at end of file From 537dfd2b0ee07b40c988cb5393f6f2787be4400f Mon Sep 17 00:00:00 2001 From: PrabhuAppalapuriSAMSA-IT Date: Wed, 19 Nov 2025 17:59:22 +0100 Subject: [PATCH 2/3] 17.0 [ADD] mail_drop_target: initial version --- mail_drop_target/README.rst | 74 ++++++++++-------- mail_drop_target/__manifest__.py | 8 +- mail_drop_target/controllers/discuss.py | 18 +++-- mail_drop_target/models/ir_attachment.py | 5 +- mail_drop_target/models/mail_thread.py | 2 +- mail_drop_target/pyproject.toml | 3 + mail_drop_target/readme/CONTRIBUTORS.md | 6 ++ mail_drop_target/readme/CONTRIBUTORS.rst | 6 -- mail_drop_target/readme/CREDITS.md | 3 + mail_drop_target/readme/CREDITS.rst | 2 - mail_drop_target/readme/DESCRIPTION.md | 10 +++ mail_drop_target/readme/DESCRIPTION.rst | 7 -- mail_drop_target/readme/ROADMAP.md | 4 + mail_drop_target/readme/ROADMAP.rst | 2 - mail_drop_target/readme/USAGE.md | 5 ++ mail_drop_target/readme/USAGE.rst | 4 - .../static/description/index.html | 78 +++++++++++-------- .../src/core/common/thread_service_patch.js | 20 ++--- .../src/js/attachment_upload_service.js | 39 ++++++---- .../views/res_config_settings_views.xml | 14 ++-- requirements.txt | 3 + 21 files changed, 185 insertions(+), 128 deletions(-) create mode 100644 mail_drop_target/pyproject.toml create mode 100644 mail_drop_target/readme/CONTRIBUTORS.md delete mode 100644 mail_drop_target/readme/CONTRIBUTORS.rst create mode 100644 mail_drop_target/readme/CREDITS.md delete mode 100644 mail_drop_target/readme/CREDITS.rst create mode 100644 mail_drop_target/readme/DESCRIPTION.md delete mode 100644 mail_drop_target/readme/DESCRIPTION.rst create mode 100644 mail_drop_target/readme/ROADMAP.md delete mode 100644 mail_drop_target/readme/ROADMAP.rst create mode 100644 mail_drop_target/readme/USAGE.md delete mode 100644 mail_drop_target/readme/USAGE.rst create mode 100644 requirements.txt diff --git a/mail_drop_target/README.rst b/mail_drop_target/README.rst index 1d0bdec97..350ab2ea1 100644 --- a/mail_drop_target/README.rst +++ b/mail_drop_target/README.rst @@ -1,8 +1,12 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ========================== Drag & drop emails to Odoo ========================== -.. +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! @@ -13,28 +17,31 @@ Drag & drop emails to Odoo .. |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-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-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_drop_target - :alt: OCA/social +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmail-lightgray.png?logo=github + :target: https://github.com/OCA/mail/tree/17.0/mail_drop_target + :alt: OCA/mail .. |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_drop_target + :target: https://translation.odoo-community.org/projects/mail-17-0/mail-17-0-mail_drop_target :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 + :target: https://runboat.odoo-community.org/builds?repo=OCA/mail&target_branch=17.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| -This module was written to allow users to drag&drop emails from their desktop to Odoo. +This module was written to allow users to drag&drop emails from their +desktop to Odoo. -It supports as well RFC822 .eml files as Outlook .msg (those only if `an extra library `_ is installed) files. +It supports as well RFC822 .eml files as Outlook .msg (those only if `an +extra library `__ is +installed) files. -When the mail is dropped to an odoo record, it will automatically send a notification -of that new message that has been added to all the existing followers. It is possible -to disable this notification. +When the mail is dropped to an odoo record, it will automatically send a +notification of that new message that has been added to all the existing +followers. It is possible to disable this notification. **Table of contents** @@ -46,22 +53,25 @@ Usage To use this module, you need to: -#. save your emails on the desktop / somewhere in the file system -#. drag them to your browser, and drop them on the chatter of the record you want to attach your email to +1. save your emails on the desktop / somewhere in the file system +2. drag them to your browser, and drop them on the chatter of the record + you want to attach your email to Known issues / Roadmap ====================== -* most mail clients won't allow you to drag mails directly from the mail client, you'll need some plugin for that -* for corporate environments, it might be feasible to support imap URLs and get the mail in question on the server side +- most mail clients won't allow you to drag mails directly from the mail + client, you'll need some plugin for that +- for corporate environments, it might be feasible to support imap URLs + and get the mail in question on the server side Bug Tracker =========== -Bugs are tracked on `GitHub Issues `_. +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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -69,27 +79,29 @@ Credits ======= Authors -~~~~~~~ +------- * Therp BV Contributors -~~~~~~~~~~~~ +------------ -* Prabhu Appalapuri -* Artiom Kichojal -* Holger Brunn -* Enric Tobella -* Lois Rilo -* Nguyen Minh Chien +- Holger Brunn +- Enric Tobella +- Lois Rilo +- Nguyen Minh Chien +- Prabhu Appalapuri +- Artiom Kichojal Other credits -~~~~~~~~~~~~~ -*The migration of this module from 16.0 to 17.0 was financially supported by SAMSA-IT -*The migration of this module from 15.0 to 16.0 was financially supported by Camptocamp +------------- + +*The migration of this module from 16.0 to 17.0 was financially +supported by SAMSA-IT*\ The migration of this module from 15.0 to 16.0 +was financially supported by Camptocamp Maintainers -~~~~~~~~~~~ +----------- This module is maintained by the OCA. @@ -101,6 +113,6 @@ 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. -This module is part of the `OCA/social `_ project on GitHub. +This module is part of the `OCA/mail `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mail_drop_target/__manifest__.py b/mail_drop_target/__manifest__.py index 15518b91e..73ada9f23 100644 --- a/mail_drop_target/__manifest__.py +++ b/mail_drop_target/__manifest__.py @@ -6,17 +6,15 @@ "author": "Therp BV,Odoo Community Association (OCA)", "license": "AGPL-3", "category": "Discuss", - "website": "https://github.com/OCA/social", + "website": "https://github.com/OCA/mail", "summary": "Attach emails to Odoo by dragging them from your desktop", "depends": ["mail"], "external_dependencies": {"python": ["extract_msg", "cryptography<37"]}, - "data": [ - "views/res_config_settings_views.xml" - ], + "data": ["views/res_config_settings_views.xml"], "assets": { "web.assets_backend": [ "mail_drop_target/static/src/core/common/thread_service_patch.js", - "mail_drop_target/static/src/js/attachment_upload_service.js" + "mail_drop_target/static/src/js/attachment_upload_service.js", ], }, } diff --git a/mail_drop_target/controllers/discuss.py b/mail_drop_target/controllers/discuss.py index 048e37d7d..93c48e0e8 100644 --- a/mail_drop_target/controllers/discuss.py +++ b/mail_drop_target/controllers/discuss.py @@ -11,22 +11,28 @@ class DiscussControllerInherit(AttachmentController): @http.route("/mail/attachment/upload", methods=["POST"], type="http", auth="public") - def mail_attachment_upload(self, ufile, thread_id, thread_model, is_pending=False, **kwargs): + def mail_attachment_upload( + self, ufile, thread_id, thread_model, is_pending=False, **kwargs + ): if not is_pending or is_pending == "false": # Add this point, make sure the message related to the uploaded # file does exist. - resp = self.mail_attachment_upload_email( - ufile, thread_id, thread_model) + resp = self.mail_attachment_upload_email(ufile, thread_id, thread_model) if resp: return resp - return super().mail_attachment_upload(ufile, thread_id, thread_model, is_pending, **kwargs) + return super().mail_attachment_upload( + ufile, thread_id, thread_model, is_pending, **kwargs + ) def mail_attachment_upload_email(self, ufile, thread_id, thread_model): channel_member = request.env["discuss.channel.member"] if thread_model == "mail.channel": - channel_member = request.env["discuss.channel.member"]._get_as_sudo_from_request_or_raise( - request=request, channel_id=int(thread_id)) + channel_member = request.env[ + "discuss.channel.member" + ]._get_as_sudo_from_request_or_raise( + request=request, channel_id=int(thread_id) + ) try: mail_resp = channel_member.env["ir.attachment"].read_mail_file_content( diff --git a/mail_drop_target/models/ir_attachment.py b/mail_drop_target/models/ir_attachment.py index 70f2c8563..9807cb799 100644 --- a/mail_drop_target/models/ir_attachment.py +++ b/mail_drop_target/models/ir_attachment.py @@ -27,8 +27,7 @@ def _process_email_file_default(self, res_obj, raw_content): if not hasattr(res_obj, "message_drop"): return False message = raw_content - thread_id = res_obj.message_drop( - res_obj._name, message, thread_id=res_obj.id) + thread_id = res_obj.message_drop(res_obj._name, message, thread_id=res_obj.id) return thread_id def read_mail_file_content(self, file_name, raw_content, res_id, res_model): @@ -42,7 +41,7 @@ def read_mail_file_content(self, file_name, raw_content, res_id, res_model): if not res_obj: return False - handler = "_process_email_file_{}".format(file_extension) + handler = f"_process_email_file_{file_extension}" if not hasattr(self, handler): handler = "_process_email_file_default" diff --git a/mail_drop_target/models/mail_thread.py b/mail_drop_target/models/mail_thread.py index 288ee2d9d..2b6a6f645 100644 --- a/mail_drop_target/models/mail_thread.py +++ b/mail_drop_target/models/mail_thread.py @@ -137,7 +137,7 @@ def _notify_thread_by_email( force_send=True, send_after_commit=True, # email send subtitles=None, - **kwargs + **kwargs, ): if self.env.context.get("message_create_from_mail_mail", False): return diff --git a/mail_drop_target/pyproject.toml b/mail_drop_target/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/mail_drop_target/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/mail_drop_target/readme/CONTRIBUTORS.md b/mail_drop_target/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..bcdcde5ae --- /dev/null +++ b/mail_drop_target/readme/CONTRIBUTORS.md @@ -0,0 +1,6 @@ +- Holger Brunn \ +- Enric Tobella \ +- Lois Rilo \ +- Nguyen Minh Chien \ +- Prabhu Appalapuri\ +- Artiom Kichojal\ diff --git a/mail_drop_target/readme/CONTRIBUTORS.rst b/mail_drop_target/readme/CONTRIBUTORS.rst deleted file mode 100644 index b1f41cc2e..000000000 --- a/mail_drop_target/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1,6 +0,0 @@ -* Holger Brunn -* Enric Tobella -* Lois Rilo -* Nguyen Minh Chien -* Prabhu Appalapuri -* Artiom Kichojal diff --git a/mail_drop_target/readme/CREDITS.md b/mail_drop_target/readme/CREDITS.md new file mode 100644 index 000000000..d28e21b9d --- /dev/null +++ b/mail_drop_target/readme/CREDITS.md @@ -0,0 +1,3 @@ +*The migration of this module from 16.0 to 17.0 was financially +supported by SAMSA-IT*The migration of this module from 15.0 to 16.0 was +financially supported by Camptocamp diff --git a/mail_drop_target/readme/CREDITS.rst b/mail_drop_target/readme/CREDITS.rst deleted file mode 100644 index cb98d23fa..000000000 --- a/mail_drop_target/readme/CREDITS.rst +++ /dev/null @@ -1,2 +0,0 @@ -*The migration of this module from 16.0 to 17.0 was financially supported by SAMSA-IT -*The migration of this module from 15.0 to 16.0 was financially supported by Camptocamp diff --git a/mail_drop_target/readme/DESCRIPTION.md b/mail_drop_target/readme/DESCRIPTION.md new file mode 100644 index 000000000..c871abc30 --- /dev/null +++ b/mail_drop_target/readme/DESCRIPTION.md @@ -0,0 +1,10 @@ +This module was written to allow users to drag&drop emails from their +desktop to Odoo. + +It supports as well RFC822 .eml files as Outlook .msg (those only if [an +extra library](https://github.com/mattgwwalker/msg-extractor) is +installed) files. + +When the mail is dropped to an odoo record, it will automatically send a +notification of that new message that has been added to all the existing +followers. It is possible to disable this notification. diff --git a/mail_drop_target/readme/DESCRIPTION.rst b/mail_drop_target/readme/DESCRIPTION.rst deleted file mode 100644 index c017ccdff..000000000 --- a/mail_drop_target/readme/DESCRIPTION.rst +++ /dev/null @@ -1,7 +0,0 @@ -This module was written to allow users to drag&drop emails from their desktop to Odoo. - -It supports as well RFC822 .eml files as Outlook .msg (those only if `an extra library `_ is installed) files. - -When the mail is dropped to an odoo record, it will automatically send a notification -of that new message that has been added to all the existing followers. It is possible -to disable this notification. diff --git a/mail_drop_target/readme/ROADMAP.md b/mail_drop_target/readme/ROADMAP.md new file mode 100644 index 000000000..49decb662 --- /dev/null +++ b/mail_drop_target/readme/ROADMAP.md @@ -0,0 +1,4 @@ +- most mail clients won't allow you to drag mails directly from the mail + client, you'll need some plugin for that +- for corporate environments, it might be feasible to support imap URLs + and get the mail in question on the server side diff --git a/mail_drop_target/readme/ROADMAP.rst b/mail_drop_target/readme/ROADMAP.rst deleted file mode 100644 index 6e2dc0854..000000000 --- a/mail_drop_target/readme/ROADMAP.rst +++ /dev/null @@ -1,2 +0,0 @@ -* most mail clients won't allow you to drag mails directly from the mail client, you'll need some plugin for that -* for corporate environments, it might be feasible to support imap URLs and get the mail in question on the server side diff --git a/mail_drop_target/readme/USAGE.md b/mail_drop_target/readme/USAGE.md new file mode 100644 index 000000000..20a9963db --- /dev/null +++ b/mail_drop_target/readme/USAGE.md @@ -0,0 +1,5 @@ +To use this module, you need to: + +1. save your emails on the desktop / somewhere in the file system +2. drag them to your browser, and drop them on the chatter of the + record you want to attach your email to diff --git a/mail_drop_target/readme/USAGE.rst b/mail_drop_target/readme/USAGE.rst deleted file mode 100644 index 080e2b51f..000000000 --- a/mail_drop_target/readme/USAGE.rst +++ /dev/null @@ -1,4 +0,0 @@ -To use this module, you need to: - -#. save your emails on the desktop / somewhere in the file system -#. drag them to your browser, and drop them on the chatter of the record you want to attach your email to diff --git a/mail_drop_target/static/description/index.html b/mail_drop_target/static/description/index.html index 012cf278d..293749857 100644 --- a/mail_drop_target/static/description/index.html +++ b/mail_drop_target/static/description/index.html @@ -3,15 +3,16 @@ -Drag & drop emails to Odoo +README.rst -
-

Drag & drop emails to Odoo

+
+ + +Odoo Community Association + +
+

Drag & drop emails to Odoo

-

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

-

This module was written to allow users to drag&drop emails from their desktop to Odoo.

-

It supports as well RFC822 .eml files as Outlook .msg (those only if an extra library is installed) files.

-

When the mail is dropped to an odoo record, it will automatically send a notification -of that new message that has been added to all the existing followers. It is possible -to disable this notification.

+

Beta License: AGPL-3 OCA/mail Translate me on Weblate Try me on Runboat

+

This module was written to allow users to drag&drop emails from their +desktop to Odoo.

+

It supports as well RFC822 .eml files as Outlook .msg (those only if an +extra library is +installed) files.

+

When the mail is dropped to an odoo record, it will automatically send a +notification of that new message that has been added to all the existing +followers. It is possible to disable this notification.

Table of contents

    @@ -390,63 +399,70 @@

    Drag & drop emails to Odoo

-

Usage

+

Usage

To use this module, you need to:

  1. save your emails on the desktop / somewhere in the file system
  2. -
  3. drag them to your browser, and drop them on the chatter of the record you want to attach your email to
  4. +
  5. drag them to your browser, and drop them on the chatter of the record +you want to attach your email to
-

Known issues / Roadmap

+

Known issues / Roadmap

    -
  • most mail clients won’t allow you to drag mails directly from the mail client, you’ll need some plugin for that
  • -
  • for corporate environments, it might be feasible to support imap URLs and get the mail in question on the server side
  • +
  • most mail clients won’t allow you to drag mails directly from the mail +client, you’ll need some plugin for that
  • +
  • for corporate environments, it might be feasible to support imap URLs +and get the mail in question on the server side
-

Bug Tracker

-

Bugs are tracked on GitHub Issues. +

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.

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Therp BV
-

Contributors

+

Contributors

-

Other credits

-

The migration of this module from 16.0 to 17.0 was financially supported by SAMSA-IT

-

The migration of this module from 15.0 to 16.0 was financially supported by Camptocamp

+

Other credits

+

The migration of this module from 16.0 to 17.0 was financially +supported by SAMSA-ITThe migration of this module from 15.0 to 16.0 +was financially supported by Camptocamp

-

Maintainers

+

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.

-

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

+

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

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

+
diff --git a/mail_drop_target/static/src/core/common/thread_service_patch.js b/mail_drop_target/static/src/core/common/thread_service_patch.js index ae8c9a498..b61bfd6cf 100644 --- a/mail_drop_target/static/src/core/common/thread_service_patch.js +++ b/mail_drop_target/static/src/core/common/thread_service_patch.js @@ -1,17 +1,16 @@ /** @odoo-module */ -import { ThreadService } from "@mail/core/common/thread_service"; -import { patch } from "@web/core/utils/patch"; -import { sortBy } from "@web/core/utils/arrays"; +import {ThreadService} from "@mail/core/common/thread_service"; +import {patch} from "@web/core/utils/patch"; +import {sortBy} from "@web/core/utils/arrays"; const FETCH_LIMIT = 30; - -patch(ThreadService.prototype, { +patch(ThreadService.prototype, { /** * @param {import("models").Thread} thread * @param {{after: Number, before: Number}} */ - async fetchMessages(thread, { after, before } = {}) { + async fetchMessages(thread, {after, before} = {}) { thread.status = "loading"; if (thread.type === "chatter" && !thread.id) { thread.isLoaded = true; @@ -19,13 +18,16 @@ patch(ThreadService.prototype, { } try { // ordered messages received: newest to oldest - const { messages: rawMessages } = await this.rpc(this.getFetchRoute(thread), { + const {messages: rawMessages} = await this.rpc(this.getFetchRoute(thread), { ...this.getFetchParams(thread), limit: FETCH_LIMIT, after, before, }); - const messages = this.store.Message.insert(sortBy(rawMessages, 'date').reverse().reverse(), { html: true }); + const messages = this.store.Message.insert( + sortBy(rawMessages, "date").reverse().reverse(), + {html: true} + ); thread.isLoaded = true; return messages; } catch (e) { @@ -34,5 +36,5 @@ patch(ThreadService.prototype, { } finally { thread.status = "ready"; } - } + }, }); diff --git a/mail_drop_target/static/src/js/attachment_upload_service.js b/mail_drop_target/static/src/js/attachment_upload_service.js index e42118d8c..794017fc1 100644 --- a/mail_drop_target/static/src/js/attachment_upload_service.js +++ b/mail_drop_target/static/src/js/attachment_upload_service.js @@ -1,11 +1,11 @@ /* @odoo-module */ -import { AttachmentUploadService } from "@mail/core/common/attachment_upload_service"; +import {AttachmentUploadService} from "@mail/core/common/attachment_upload_service"; -import { patch } from "@web/core/utils/patch"; -import { url } from "@web/core/utils/urls"; -import { _t } from "@web/core/l10n/translation"; -import { Deferred } from "@web/core/utils/concurrency"; +import {patch} from "@web/core/utils/patch"; +import {url} from "@web/core/utils/urls"; +import {_t} from "@web/core/l10n/translation"; +import {Deferred} from "@web/core/utils/concurrency"; patch(AttachmentUploadService.prototype, { setup(env, services) { @@ -24,7 +24,7 @@ patch(AttachmentUploadService.prototype, { this.fileUploadService.bus.addEventListener( "FILE_UPLOAD_ADDED", - ({ detail: { upload } }) => { + ({detail: {upload}}) => { const tmpId = parseInt(upload.data.get("temporary_id")); if (!this.uploadingAttachmentIds.has(tmpId)) { return; @@ -53,7 +53,7 @@ patch(AttachmentUploadService.prototype, { ); this.fileUploadService.bus.addEventListener( "FILE_UPLOAD_LOADED", - ({ detail: { upload } }) => { + ({detail: {upload}}) => { const tmpId = parseInt(upload.data.get("temporary_id")); if (!this.uploadingAttachmentIds.has(tmpId)) { return; @@ -63,36 +63,43 @@ patch(AttachmentUploadService.prototype, { this.abortByAttachmentId.delete(tmpId); const hooker = this.hookersByTmpId.get(tmpId); if (upload.xhr.status === 413) { - this.notificationService.add(_t("File too large"), { type: "danger" }); + this.notificationService.add(_t("File too large"), { + type: "danger", + }); this.hookersByTmpId.delete(tmpId); return; } if (upload.xhr.status !== 200) { - this.notificationService.add(_t("Server error"), { type: "danger" }); + this.notificationService.add(_t("Server error"), {type: "danger"}); this.hookersByTmpId.delete(tmpId); return; } const response = JSON.parse(upload.xhr.response); if (response.error) { - this.notificationService.add(response.error, { type: "danger" }); + this.notificationService.add(response.error, {type: "danger"}); this.hookersByTmpId.delete(tmpId); return; } let xhrResp = JSON.parse(upload.xhr.response); - if(xhrResp.email_upload){ + if (xhrResp.email_upload) { location.reload(); - return + return; } const threadId = parseInt(upload.data.get("thread_id")); const threadModel = upload.data.get("thread_model"); - const originThread = this.store.Thread.get({ model: threadModel, id: threadId }); + const originThread = this.store.Thread.get({ + model: threadModel, + id: threadId, + }); const attachment = this.store.Attachment.insert({ ...response, extension: upload.title.split(".").pop(), originThread: hooker.composer ? undefined : originThread, }); if (hooker.composer) { - const index = hooker.composer.attachments.findIndex(({ id }) => id === tmpId); + const index = hooker.composer.attachments.findIndex( + ({id}) => id === tmpId + ); if (index >= 0) { hooker.composer.attachments[index] = attachment; } else { @@ -111,7 +118,7 @@ patch(AttachmentUploadService.prototype, { ); this.fileUploadService.bus.addEventListener( "FILE_UPLOAD_ERROR", - ({ detail: { upload } }) => { + ({detail: {upload}}) => { const tmpId = parseInt(upload.data.get("temporary_id")); if (!this.uploadingAttachmentIds.has(tmpId)) { return; @@ -122,5 +129,5 @@ patch(AttachmentUploadService.prototype, { this.hookersByTmpId.delete(tmpId); } ); - } + }, }); diff --git a/mail_drop_target/views/res_config_settings_views.xml b/mail_drop_target/views/res_config_settings_views.xml index 1563ef7fb..93c383d4e 100644 --- a/mail_drop_target/views/res_config_settings_views.xml +++ b/mail_drop_target/views/res_config_settings_views.xml @@ -1,15 +1,19 @@ - + res.config.settings.view.form.inherit.mail res.config.settings - + - - + + - \ No newline at end of file + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..e4ecac191 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# generated from manifests external_dependencies +cryptography<37 +extract_msg From 4cf0e954703b20f0d61897ad60c3822e570b6586 Mon Sep 17 00:00:00 2001 From: PrabhuAppalapuriSAMSA-IT Date: Thu, 20 Nov 2025 09:26:32 +0100 Subject: [PATCH 3/3] 17.0 [FIX] mail_drop_target: pre commit recommnedations fix --- mail_drop_target/__init__.py | 2 ++ mail_drop_target/__manifest__.py | 4 ++-- ...vice_patch.js => thread_service_patch.esm.js} | 4 ++-- ...rvice.js => attachment_upload_service.esm.js} | 16 ++++++++-------- 4 files changed, 14 insertions(+), 12 deletions(-) rename mail_drop_target/static/src/core/common/{thread_service_patch.js => thread_service_patch.esm.js} (93%) rename mail_drop_target/static/src/js/{attachment_upload_service.js => attachment_upload_service.esm.js} (94%) diff --git a/mail_drop_target/__init__.py b/mail_drop_target/__init__.py index 9484ea350..9cb98d97c 100644 --- a/mail_drop_target/__init__.py +++ b/mail_drop_target/__init__.py @@ -1,4 +1,6 @@ # Copyright 2018 Therp BV # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + from . import controllers from . import models diff --git a/mail_drop_target/__manifest__.py b/mail_drop_target/__manifest__.py index 73ada9f23..dd6d4bc82 100644 --- a/mail_drop_target/__manifest__.py +++ b/mail_drop_target/__manifest__.py @@ -13,8 +13,8 @@ "data": ["views/res_config_settings_views.xml"], "assets": { "web.assets_backend": [ - "mail_drop_target/static/src/core/common/thread_service_patch.js", - "mail_drop_target/static/src/js/attachment_upload_service.js", + "mail_drop_target/static/src/core/common/thread_service_patch.esm.js", + "mail_drop_target/static/src/js/attachment_upload_service.esm.js", ], }, } diff --git a/mail_drop_target/static/src/core/common/thread_service_patch.js b/mail_drop_target/static/src/core/common/thread_service_patch.esm.js similarity index 93% rename from mail_drop_target/static/src/core/common/thread_service_patch.js rename to mail_drop_target/static/src/core/common/thread_service_patch.esm.js index b61bfd6cf..fb4ecbc08 100644 --- a/mail_drop_target/static/src/core/common/thread_service_patch.js +++ b/mail_drop_target/static/src/core/common/thread_service_patch.esm.js @@ -1,4 +1,4 @@ -/** @odoo-module */ +/** @odoo-module **/ import {ThreadService} from "@mail/core/common/thread_service"; import {patch} from "@web/core/utils/patch"; @@ -17,7 +17,7 @@ patch(ThreadService.prototype, { return []; } try { - // ordered messages received: newest to oldest + // Ordered messages received: newest to oldest const {messages: rawMessages} = await this.rpc(this.getFetchRoute(thread), { ...this.getFetchParams(thread), limit: FETCH_LIMIT, diff --git a/mail_drop_target/static/src/js/attachment_upload_service.js b/mail_drop_target/static/src/js/attachment_upload_service.esm.js similarity index 94% rename from mail_drop_target/static/src/js/attachment_upload_service.js rename to mail_drop_target/static/src/js/attachment_upload_service.esm.js index 794017fc1..ffbf01500 100644 --- a/mail_drop_target/static/src/js/attachment_upload_service.js +++ b/mail_drop_target/static/src/js/attachment_upload_service.esm.js @@ -1,20 +1,20 @@ -/* @odoo-module */ +/** @odoo-module **/ import {AttachmentUploadService} from "@mail/core/common/attachment_upload_service"; import {patch} from "@web/core/utils/patch"; -import {url} from "@web/core/utils/urls"; +// Import {url} from "@web/core/utils/urls"; import {_t} from "@web/core/l10n/translation"; -import {Deferred} from "@web/core/utils/concurrency"; +// Import {Deferred} from "@web/core/utils/concurrency"; patch(AttachmentUploadService.prototype, { setup(env, services) { this.env = env; - this.fileUploadService = services["file_upload"]; - /** @type {import("@mail/core/common/store_service").Store} */ + this.fileUploadService = services.file_upload; + /** @type {import("@mail/core/common/store_service").Store} **/ this.store = services["mail.store"]; - this.notificationService = services["notification"]; - /** @type {import("@mail/core/common/attachment_service").AttachmentService} */ + this.notificationService = services.notification; + /** @type {import("@mail/core/common/attachment_service").AttachmentService} **/ this.attachmentService = services["mail.attachment"]; this.abortByAttachmentId = new Map(); @@ -80,7 +80,7 @@ patch(AttachmentUploadService.prototype, { this.hookersByTmpId.delete(tmpId); return; } - let xhrResp = JSON.parse(upload.xhr.response); + const xhrResp = JSON.parse(upload.xhr.response); if (xhrResp.email_upload) { location.reload(); return;