diff --git a/msg_attachments_to_mail_message/__init__.py b/msg_attachments_to_mail_message/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/msg_attachments_to_mail_message/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/msg_attachments_to_mail_message/__manifest__.py b/msg_attachments_to_mail_message/__manifest__.py new file mode 100644 index 0000000..40be6c7 --- /dev/null +++ b/msg_attachments_to_mail_message/__manifest__.py @@ -0,0 +1,61 @@ +{ + 'name': 'MSG to Mail Message', + 'version': '18.0.1.1', + 'category': 'Mail', + 'summary': 'Process MSG files as incoming emails', + 'license': 'LGPL-3', + 'description': """ + This module processes Outlook files (.msg) as incoming emails in Odoo when upload as attachments. + + Features + ======== + 1. Automatic MSG File Detection + - Identifies .msg files by extension or MIME type + - Automatic processing upon upload + + 2. Message Content Extraction + - Email subject + - Message body + - Sender information + - Send date + - Attachments + + 3. Odoo Message Creation + - Creates a mail.message in the chatter + - Preserves all original email information + - Integrates the message into existing discussion thread + - Don't add email receipent as follower, just add message and attachments + + 4. Attachment Management + - Automatically extracts all attachments from MSG file + - Creates standard Odoo attachments for each file + - Links attachments to the created message + - Preserves original filenames + + Usage + ===== + - Simply upload a .msg file to Odoo + - The module automatically processes the file + - Message appears in the discussion thread + - Attachments are accessible as standard attachments + + Technical Notes + =============== + Requires Python library 'extract-msg' for MSG file processing. + """, + 'author': 'Bemade inc.', + 'website': 'https://www.bemade.org', + 'depends': [ + 'base', + 'mail', + ], + 'data': [ + 'views/res_config_settings_views.xml', + ], + 'external_dependencies': { + 'python': ['extract-msg'], + }, + 'installable': True, + 'auto_install': False, + 'application': False, +} \ No newline at end of file diff --git a/msg_attachments_to_mail_message/migrations/16.0.1.1/__init__.py b/msg_attachments_to_mail_message/migrations/16.0.1.1/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/msg_attachments_to_mail_message/migrations/16.0.1.1/__init__.py @@ -0,0 +1 @@ + diff --git a/msg_attachments_to_mail_message/migrations/16.0.1.1/pre-migration.py b/msg_attachments_to_mail_message/migrations/16.0.1.1/pre-migration.py new file mode 100644 index 0000000..7039db2 --- /dev/null +++ b/msg_attachments_to_mail_message/migrations/16.0.1.1/pre-migration.py @@ -0,0 +1,36 @@ +from odoo import SUPERUSER_ID, api + +def migrate(cr, version): + if not version: + return + + # Add msg_processed column + cr.execute(""" + ALTER TABLE ir_attachment + ADD COLUMN IF NOT EXISTS msg_processed boolean DEFAULT false + """) + + # Add mail_message_id column + cr.execute(""" + ALTER TABLE ir_attachment + ADD COLUMN IF NOT EXISTS mail_message_id integer + """) + + # Add foreign key constraint + cr.execute(""" + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.constraint_column_usage + WHERE table_name = 'ir_attachment' + AND column_name = 'mail_message_id' + AND constraint_name = 'ir_attachment_mail_message_id_fkey' + ) THEN + ALTER TABLE ir_attachment + ADD CONSTRAINT ir_attachment_mail_message_id_fkey + FOREIGN KEY (mail_message_id) + REFERENCES mail_message(id) ON DELETE SET NULL; + END IF; + END $$; + """) diff --git a/msg_attachments_to_mail_message/migrations/__init__.py b/msg_attachments_to_mail_message/migrations/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/msg_attachments_to_mail_message/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/msg_attachments_to_mail_message/models/__init__.py b/msg_attachments_to_mail_message/models/__init__.py new file mode 100644 index 0000000..075fb0e --- /dev/null +++ b/msg_attachments_to_mail_message/models/__init__.py @@ -0,0 +1,2 @@ +from . import ir_attachment +from . import res_config_settings diff --git a/msg_attachments_to_mail_message/models/ir_attachment.py b/msg_attachments_to_mail_message/models/ir_attachment.py new file mode 100644 index 0000000..159807e --- /dev/null +++ b/msg_attachments_to_mail_message/models/ir_attachment.py @@ -0,0 +1,290 @@ +from odoo import models, api, fields +from odoo.tools import html_sanitize +import extract_msg +import base64 +import logging +from odoo.tools.translate import _ +from email.message import EmailMessage +import lxml.html +import re +from odoo.tools import email_normalize, email_split + +_logger = logging.getLogger(__name__) + +class IrAttachment(models.Model): + _inherit = 'ir.attachment' + + msg_processed = fields.Boolean(string='MSG Processed', default=False) + mail_message_id = fields.Many2one('mail.message', string='Created Mail Message') + + def _is_msg_file(self): + """Check if the attachment is an MSG file and validate size.""" + self.ensure_one() + + # Check if it's an MSG file + is_msg = ( + self.mimetype == 'application/vnd.ms-outlook' or + (self.name and self.name.lower().endswith('.msg')) + ) + + if not is_msg: + return False + + # Check file size + max_size = int(self.env['ir.config_parameter'].sudo().get_param( + 'msg_attachments.max_file_size', '25')) + file_size_mb = len(self.datas) * 3 / 4 / 1024 / 1024 # Convert from base64 to MB + + if file_size_mb > max_size: + self._notify_error('File Size Error', + _('The MSG file is too large. Maximum allowed size is %s MB') % max_size) + return False + + return True + + def _clean_header_value(self, value): + """Clean header value by removing line breaks and extra whitespace.""" + if not value: + return '' + # Replace any combination of whitespace (including newlines) with a single space + return ' '.join(str(value).split()) + + def process_msg_as_email(self): + """Convert MSG file to standard email format and process it using Odoo's mail module.""" + self.ensure_one() + _logger.info('Starting MSG processing for file: %s (model: %s, res_id: %s)', + self.name, self.res_model, self.res_id) + + if not self._is_msg_file(): + _logger.warning('File %s is not a valid MSG file', self.name) + self._notify_error('Invalid File', + _('The selected file is not a valid MSG file')) + return False + + if not self.datas: + try: + # Try to reload data from filestore + self.datas = self.datas + except Exception as e: + _logger.error('Failed to read file %s from filestore: %s', self.name, str(e)) + self._notify_error('File Access Error', + _('The MSG file could not be read from the filestore. The file may have been moved or deleted.')) + return False + + try: + # Read the MSG file + _logger.info('Decoding MSG file data') + msg_data = base64.b64decode(self.datas) + msg_file = extract_msg.Message(msg_data) + _logger.info('MSG file info - Subject: %s, From: %s, To: %s', + msg_file.subject, msg_file.sender, msg_file.to) + + # Convert MSG to email format + email_msg = EmailMessage() + email_msg['Subject'] = self._clean_header_value(msg_file.subject) or _("No Subject") + email_msg['From'] = self._clean_header_value(msg_file.sender) + email_msg['To'] = self._clean_header_value(msg_file.to) + email_msg['Cc'] = self._clean_header_value(msg_file.cc) + email_msg['Date'] = msg_file.date.strftime('%a, %d %b %Y %H:%M:%S %z') if msg_file.date else '' + email_msg['References'] = self._clean_header_value(msg_file.header.get('References', '')) + email_msg['In-Reply-To'] = self._clean_header_value(msg_file.header.get('In-Reply-To', '')) + email_msg['Message-ID'] = self._clean_header_value(msg_file.header.get('Message-Id', '')) + + # Add X-Headers to force message association + if self.res_model and self.res_id: + email_msg['X-Odoo-Objects'] = f'{self.res_model}-{self.res_id}' + + # Set the body + if msg_file.body: + _logger.info('Processing message body') + # Convert [cid:xxx] to proper HTML img tags + body = msg_file.body + cid_pattern = r'\[cid:([^\]]+)\]' + body = re.sub(cid_pattern, r'', body) + # Nettoyer le HTML si nécessaire + if ' + cid = attachment.content_id.strip('<>') + headers['Content-ID'] = f'<{cid}>' + + # Create Odoo attachment for all attachments, not just those with CID + try: + odoo_attachment = self.env['ir.attachment'].create({ + 'name': filename or 'unknown.bin', + 'datas': base64.b64encode(attachment.data), + 'mimetype': attachment.mimetype or 'application/octet-stream', + 'res_model': self.res_model, + 'res_id': self.res_id, + }) + _logger.info('Created Odoo attachment: %s (ID: %s)', + odoo_attachment.name, odoo_attachment.id) + + attachment_ids.append(odoo_attachment.id) + if headers.get('Content-ID'): + attachment_mapping[cid] = odoo_attachment.id + except Exception as e: + _logger.error('Failed to create attachment %s: %s', filename, str(e)) + + email_msg.add_attachment( + attachment.data, + maintype=maintype, + subtype=subtype, + filename=filename or 'unknown.bin', + headers=headers + ) + + # Process using Odoo's standard mail handling + thread_model = self.env[self.res_model] if self.res_model else self.env['mail.thread'] + _logger.info('Using thread model: %s', thread_model._name) + + # Forcer le contexte pour la création du message + context = { + 'default_model': self.res_model, + 'default_res_id': self.res_id, + 'mail_create_nosubscribe': True, + 'mail_create_nolog': True, + 'mail_notify_force_send': False, + 'mail_create_force_model': self.res_model, + 'mail_thread_quote': False, + } + _logger.info('Setting context for message creation: %s', context) + thread_model = thread_model.with_context(**context) + + # Créer directement le message si nous avons un modèle et un ID + if self.res_model and self.res_id: + _logger.info('Creating message directly for model %s with ID %s', + self.res_model, self.res_id) + # Trouver l'auteur du message + author_id = False + if msg_file.sender: + email_normalized = email_normalize(msg_file.sender) + _logger.info('Looking for partner with email: %s', email_normalized) + partner = self.env['res.partner'].sudo().search([ + ('email_normalized', '=', email_normalized) + ], limit=1) + if partner: + author_id = partner.id + _logger.info('Found partner: %s (ID: %s)', partner.name, partner.id) + else: + _logger.info('No partner found for email %s', email_normalized) + + # Créer le message directement + try: + vals = { + 'subject': msg_file.subject or _("No Subject"), + 'body': body if msg_file.body else '', + 'email_from': msg_file.sender, + 'author_id': author_id, + 'message_type': 'email', + 'model': self.res_model, + 'res_id': self.res_id, + 'attachment_ids': [(6, 0, attachment_ids)], + } + _logger.info('Creating mail.message with values: %s', vals) + message = self.env['mail.message'].create(vals) + _logger.info('Created message successfully: ID %s', message.id) + + # Marquer le fichier MSG comme traité + self.write({ + 'description': _('Processed as email on %s') % fields.Datetime.now(), + 'msg_processed': True, + 'mail_message_id': message.id, + }) + _logger.info('MSG file marked as processed') + + return message.id + except Exception as e: + _logger.error('Failed to create message: %s', str(e), exc_info=True) + raise + else: + _logger.info('No model/ID available, falling back to message_process') + # Si nous n'avons pas de modèle/ID, utiliser message_process + thread_id = thread_model.message_process( + model=self.res_model, + message=email_msg.as_bytes(), + custom_values={ + 'res_id': self.res_id, + 'model': self.res_model, + 'msg_attachment_id': self.id, + }, + thread_id=self.res_id + ) + + if thread_id: + _logger.info('Message processed successfully, thread_id: %s', thread_id) + # Trouver le message créé + message = self.env['mail.message'].search([ + ('model', '=', self.res_model), + ('res_id', '=', thread_id) + ], order='id desc', limit=1) + + if message: + _logger.info('Found created message: ID %s', message.id) + else: + _logger.warning('No message found after processing') + + # Marquer le fichier MSG comme traité + self.write({ + 'description': _('Processed as email on %s') % fields.Datetime.now(), + 'msg_processed': True, + 'mail_message_id': message.id if message else False, + }) + _logger.info('MSG file marked as processed') + else: + _logger.error('message_process failed to create thread_id') + + return thread_id + + except Exception as e: + _logger.error('Error processing MSG file %s: %s', self.name, str(e), exc_info=True) + self._notify_error('Processing Error', + _('Could not process the MSG file: %s') % str(e)) + return False + + def _notify_error(self, title, message): + """Show error notification to the user.""" + self.env['bus.bus']._sendone(self.env.user.partner_id, 'notification', { + 'type': 'danger', + 'title': title, + 'message': message, + 'sticky': True, + }) + + @api.model_create_multi + def create(self, vals_list): + attachments = super().create(vals_list) + for attachment in attachments: + if attachment._is_msg_file(): + attachment.process_msg_as_email() + return attachments diff --git a/msg_attachments_to_mail_message/models/res_config_settings.py b/msg_attachments_to_mail_message/models/res_config_settings.py new file mode 100644 index 0000000..d803db7 --- /dev/null +++ b/msg_attachments_to_mail_message/models/res_config_settings.py @@ -0,0 +1,32 @@ +from odoo import fields, models + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + msg_max_file_size = fields.Integer( + string='Maximum MSG File Size (MB)', + config_parameter='msg_attachments.max_file_size', + default=25 + ) + + msg_format_metadata = fields.Boolean( + string='Show Email Metadata in Message', + config_parameter='msg_attachments.format_metadata', + default=True + ) + + msg_attachment_mode = fields.Selection([ + ('all', 'Process All Attachments'), + ('skip_empty', 'Skip Empty Attachments'), + ('selective', 'Process Selected Types Only') + ], string='Attachment Processing Mode', + config_parameter='msg_attachments.attachment_mode', + default='all' + ) + + msg_allowed_extensions = fields.Char( + string='Allowed Attachment Extensions', + config_parameter='msg_attachments.allowed_extensions', + default='.pdf,.doc,.docx,.xls,.xlsx,.jpg,.png', + help='Comma-separated list of allowed file extensions' + ) diff --git a/msg_attachments_to_mail_message/views/res_config_settings_views.xml b/msg_attachments_to_mail_message/views/res_config_settings_views.xml new file mode 100644 index 0000000..83a69e2 --- /dev/null +++ b/msg_attachments_to_mail_message/views/res_config_settings_views.xml @@ -0,0 +1,44 @@ + + + + res.config.settings.view.form.inherit.msg.attachments + res.config.settings + + + + + + +
+
+ + MB +
+
+
+ + + + + + +
+
+ +
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
diff --git a/msg_viewer/__manifest__.py b/msg_viewer/__manifest__.py index af3f51e..5578e28 100644 --- a/msg_viewer/__manifest__.py +++ b/msg_viewer/__manifest__.py @@ -14,27 +14,24 @@ 'website': 'https://www.bemade.org', 'depends': [ 'base', - 'web', - 'mail' + 'web', + 'mail', + 'documents' ], 'data': [ 'views/msg_viewer_views.xml', ], 'assets': { 'web.assets_backend': [ - 'msg_viewer/static/src/js/msg_viewer.js', - 'msg_viewer/static/src/xml/msg_viewer.xml', - # 'msg_viewer/static/src/css/msg_viewer.css', - ], - 'msg_viewer.assets': [ - # MSG Viewer library files - 'msg_viewer/static/src/lib/msg-viewer.js', - 'msg_viewer/static/src/lib/scripts/**/*', - 'msg_viewer/static/src/lib/components/**/*', - 'msg_viewer/static/src/lib/styles/**/*', - ], - 'web.assets_qweb': [ - 'msg_viewer/static/src/xml/msg_viewer.xml', + # MSG viewer components + 'msg_viewer/static/src/models/attachment_card_msg.js', + 'msg_viewer/static/src/components/**/*', + + # CSS + 'msg_viewer/static/src/css/**/*', + + # MSG Viewer library + ('include', 'msg_viewer/static/src/lib/msg-viewer-main/dist/**/*'), ], }, 'installable': True, diff --git a/msg_viewer/static/src/components/attachment_card_msg/attachment_card_msg.xml b/msg_viewer/static/src/components/attachment_card_msg/attachment_card_msg.xml new file mode 100644 index 0000000..d657ecc --- /dev/null +++ b/msg_viewer/static/src/components/attachment_card_msg/attachment_card_msg.xml @@ -0,0 +1,16 @@ + + + + +
+ +
+
+
+
diff --git a/msg_viewer/static/src/components/msg_viewer/msg_viewer.js b/msg_viewer/static/src/components/msg_viewer/msg_viewer.js new file mode 100644 index 0000000..0f44189 --- /dev/null +++ b/msg_viewer/static/src/components/msg_viewer/msg_viewer.js @@ -0,0 +1,72 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { Component, onMounted } from "@odoo/owl"; +import { _t } from "@web/core/l10n/translation"; +import { Dialog } from "@web/core/dialog/dialog"; + +export class MsgViewerAction extends Component { + static template = "msg_viewer.Viewer"; + static components = { Dialog }; + + setup() { + this.orm = useService("orm"); + this.action = useService("action"); + this.notification = useService("notification"); + console.log("MsgViewerAction setup", this.props); + + onMounted(() => this.initMsgViewer()); + } + + async initMsgViewer() { + try { + console.log("Initializing MSG viewer"); + const { model, id, filename } = this.props.action.params; + console.log("Params:", { model, id, filename }); + + // Get the MSG file content from the server + console.log("Fetching content from server..."); + const data = await this.orm.call( + model, + 'read', + [[id], ['datas']], + { context: { bin_size: false } } + ); + console.log("Got data from server:", data); + + if (!data || !data.length || !data[0].datas) { + throw new Error("No data received from server"); + } + + // Initialize the MSG viewer + console.log("Creating viewer instance..."); + const container = this.el.querySelector("#msg-viewer-container"); + console.log("Container:", container); + + if (!container) { + throw new Error("MSG viewer container not found"); + } + + if (!window.MsgViewer) { + throw new Error("MSG viewer library not loaded"); + } + + const viewer = new window.MsgViewer({ + data: data[0].datas, + container: container, + }); + console.log("Viewer instance created:", viewer); + + } catch (error) { + console.error("Error initializing MSG viewer:", error); + this.notification.add(_t("Failed to initialize MSG viewer"), { + type: "danger", + }); + // Close the modal + this.action.doAction({ type: "ir.actions.act_window_close" }); + } + } +} + +registry.category("actions").add("msg_viewer", MsgViewerAction); diff --git a/msg_viewer/static/src/components/msg_viewer/msg_viewer.xml b/msg_viewer/static/src/components/msg_viewer/msg_viewer.xml new file mode 100644 index 0000000..0b11025 --- /dev/null +++ b/msg_viewer/static/src/components/msg_viewer/msg_viewer.xml @@ -0,0 +1,16 @@ + + + + + +
+
+
+ + + +
+
+
diff --git a/msg_viewer/static/src/css/msg_viewer.css b/msg_viewer/static/src/css/msg_viewer.css index 667440c..9f23b8d 100644 --- a/msg_viewer/static/src/css/msg_viewer.css +++ b/msg_viewer/static/src/css/msg_viewer.css @@ -2,6 +2,12 @@ position: relative; width: 100%; height: 600px; + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.375rem 0.75rem; + border: 1px solid #dee2e6; + border-radius: 0.25rem; } .o_field_msg_viewer_container iframe { @@ -10,3 +16,33 @@ border: 1px solid #dee2e6; border-radius: 4px; } + +.o_field_msg_viewer_container .fa { + color: #6c757d; +} + +.o_field_msg_viewer_container:hover { + background-color: #f8f9fa; + cursor: pointer; +} + +.o_msg_viewer { + width: 100%; + height: 100%; + padding: 16px; + overflow: auto; +} + +#msg-viewer-container { + width: 100%; + height: 100%; + background: white; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.o_preview_file_button { + color: var(--primary); + padding: 0; + margin: 0; +} diff --git a/msg_viewer/static/src/js/msg_viewer.js b/msg_viewer/static/src/js/msg_viewer.js index c82fdae..cfeeb14 100644 --- a/msg_viewer/static/src/js/msg_viewer.js +++ b/msg_viewer/static/src/js/msg_viewer.js @@ -1,88 +1,49 @@ -/** @odoo-module */ +/** @odoo-module **/ -import { registry } from "@web/core/registry"; +import { AttachmentList } from "@mail/core/common/attachment_list"; +import { patch } from "@web/core/utils/patch"; import { _t } from "@web/core/l10n/translation"; -import { standardFieldProps } from "@web/views/fields/standard_field_props"; -import { FileUploader } from "@web/views/fields/file_handler"; -import { Component, onWillUpdateProps, useState } from "@odoo/owl"; -import { url } from "@web/core/utils/urls"; import { useService } from "@web/core/utils/hooks"; -class MsgViewerField extends Component { - static template = "web.MsgViewerField"; - static components = { - FileUploader, - }; - static props = { - ...standardFieldProps, - }; +console.log("Loading msg_viewer.js"); +patch(AttachmentList.prototype, { setup() { + this._super(...arguments); + this.action = useService("action"); this.notification = useService("notification"); - this.state = useState({ - isValid: true, - objectUrl: "", - showViewer: false, - }); - onWillUpdateProps((nextProps) => { - if (nextProps.readonly) { - this.state.objectUrl = ""; - this.state.showViewer = false; - } - }); - } + console.log("MsgViewer patch setup called"); + }, - get url() { - if (!this.state.isValid || !this.props.value) { - return null; - } - const file = encodeURIComponent( - this.state.objectUrl || - url("/msg_viewer/content", { - model: this.props.record.resModel, - field: this.props.name, - id: this.props.record.resId, - }) - ); - return `/msg_viewer/static/src/lib/index.html?file=${file}`; - } + isMsgFile(attachment) { + console.log("Checking if is MSG file:", attachment); + return attachment.mimetype === "application/vnd.ms-outlook" || + (attachment.name && attachment.name.toLowerCase().endsWith('.msg')); + }, - async onFileUploaded(file) { - this.state.objectUrl = URL.createObjectURL(file); - await this.props.update(file); - } - - async onFileRemove() { - if (this.state.objectUrl) { - URL.revokeObjectURL(this.state.objectUrl); - this.state.objectUrl = ""; - } - this.state.showViewer = false; - await this.props.update(false); - } - - openMsgViewer() { - this.state.showViewer = true; - } - - onLoadFailed() { - this.notification.add( - _t("Could not display the selected MSG file."), - { + async openMsgViewer(attachment) { + console.log("Opening MSG viewer for:", attachment); + try { + const action = { + type: "ir.actions.client", + tag: "msg_viewer", + name: _t("MSG Viewer"), + target: "new", + params: { + model: attachment.resModel, + id: attachment.id, + filename: attachment.name, + }, + }; + console.log("Executing action:", action); + await this.action.doAction(action); + } catch (error) { + console.error("Error in openMsgViewer:", error); + this.notification.add(_t("Failed to open MSG viewer"), { type: "danger", - } - ); - this.state.isValid = false; - } -} + }); + } + }, +}); -export const msgViewerField = { - component: MsgViewerField, - displayName: _t("MSG Viewer"), - supportedTypes: ["binary"], - extractProps: ({ props }) => ({ - acceptedFileExtensions: ".msg", - }), -}; - -registry.category("fields").add("msg_viewer", msgViewerField); +console.log("MSG viewer patch registered"); diff --git a/msg_viewer/static/src/models/attachment_card_msg.js b/msg_viewer/static/src/models/attachment_card_msg.js new file mode 100644 index 0000000..3099b48 --- /dev/null +++ b/msg_viewer/static/src/models/attachment_card_msg.js @@ -0,0 +1,50 @@ +/** @odoo-module **/ + +import { AttachmentList } from "@mail/core/common/attachment_list"; +import { patch } from "@web/core/utils/patch"; +import { _t } from "@web/core/l10n/translation"; +import { useService } from "@web/core/utils/hooks"; + +console.log("Loading MSG viewer patch"); + +patch(AttachmentList.prototype, { + setup() { + super.setup(...arguments); + this.orm = useService("orm"); + this.action = useService("action"); + this.notification = useService("notification"); + console.log("MsgViewer patch setup called"); + }, + + isMsgFile(attachment) { + console.log("Checking if is MSG file:", attachment); + return attachment.mimetype === "application/vnd.ms-outlook" || + (attachment.name && attachment.name.toLowerCase().endsWith('.msg')); + }, + + async openMsgViewer(attachment) { + console.log("Opening MSG viewer for:", attachment); + try { + const action = { + type: "ir.actions.client", + tag: "msg_viewer", + name: _t("MSG Viewer"), + target: "new", + params: { + model: "ir.attachment", + id: attachment.id, + filename: attachment.name, + }, + }; + console.log("Executing action:", action); + await this.action.doAction(action); + } catch (error) { + console.error("Error in openMsgViewer:", error); + this.notification.add(_t("Failed to open MSG viewer"), { + type: "danger", + }); + } + }, +}); + +console.log("MSG viewer patch registered"); diff --git a/msg_viewer/static/src/xml/msg_viewer.xml b/msg_viewer/static/src/xml/msg_viewer.xml deleted file mode 100644 index 5bf31eb..0000000 --- a/msg_viewer/static/src/xml/msg_viewer.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - -
- - - -
-
- -
- -
-
- -
-