msg_attachments_to_mail_message
This commit is contained in:
parent
98b5ed31fd
commit
aa4fbe72ec
18 changed files with 726 additions and 145 deletions
1
msg_attachments_to_mail_message/__init__.py
Normal file
1
msg_attachments_to_mail_message/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
61
msg_attachments_to_mail_message/__manifest__.py
Normal file
61
msg_attachments_to_mail_message/__manifest__.py
Normal file
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -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 $$;
|
||||
""")
|
||||
1
msg_attachments_to_mail_message/migrations/__init__.py
Normal file
1
msg_attachments_to_mail_message/migrations/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
2
msg_attachments_to_mail_message/models/__init__.py
Normal file
2
msg_attachments_to_mail_message/models/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import ir_attachment
|
||||
from . import res_config_settings
|
||||
290
msg_attachments_to_mail_message/models/ir_attachment.py
Normal file
290
msg_attachments_to_mail_message/models/ir_attachment.py
Normal file
|
|
@ -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'<img src="cid:\1">', body)
|
||||
# Nettoyer le HTML si nécessaire
|
||||
if '<html' in body.lower():
|
||||
body = html_sanitize(body)
|
||||
email_msg.set_content(body, subtype='html')
|
||||
_logger.debug('Message body length: %d characters', len(body))
|
||||
else:
|
||||
_logger.warning('No message body found')
|
||||
email_msg.set_content('', subtype='html')
|
||||
|
||||
# Add attachments
|
||||
attachment_mapping = {}
|
||||
attachment_ids = []
|
||||
_logger.info('Processing %d attachments', len(msg_file.attachments))
|
||||
for idx, attachment in enumerate(msg_file.attachments, 1):
|
||||
if not hasattr(attachment, 'data') or not attachment.data:
|
||||
_logger.warning('Attachment %d has no data, skipping', idx)
|
||||
continue
|
||||
|
||||
maintype, subtype = (attachment.mimetype or 'application/octet-stream').split('/', 1)
|
||||
headers = {}
|
||||
|
||||
filename = None
|
||||
if hasattr(attachment, 'filename'):
|
||||
filename = attachment.filename
|
||||
elif hasattr(attachment, 'longFilename'):
|
||||
filename = attachment.longFilename
|
||||
elif hasattr(attachment, 'shortFilename'):
|
||||
filename = attachment.shortFilename
|
||||
|
||||
_logger.info('Processing attachment %d: %s (%s/%s)',
|
||||
idx, filename or 'unknown.bin', maintype, subtype)
|
||||
|
||||
if hasattr(attachment, 'content_id') and attachment.content_id:
|
||||
# Ensure Content-ID is properly formatted with <>
|
||||
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
|
||||
|
|
@ -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'
|
||||
)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.msg.attachments</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form" position="inside">
|
||||
<app data-string="MSG Attachments" string="MSG Attachments" name="msg_attachments" id="msg_attachments">
|
||||
<block title="MSG File Processing" name="msg_file_processing">
|
||||
<setting string="Maximum File Size" help="Maximum size for MSG files in megabytes">
|
||||
<div class="content-group">
|
||||
<div class="mt16">
|
||||
<field name="msg_max_file_size" class="o_light_label"/>
|
||||
<span> MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
|
||||
<setting string="Show Email Metadata" help="Display sender and date information in processed messages">
|
||||
<field name="msg_format_metadata"/>
|
||||
</setting>
|
||||
|
||||
<setting string="Attachment Processing" help="Configure how attachments should be processed">
|
||||
<div class="content-group">
|
||||
<div class="mt16">
|
||||
<field name="msg_attachment_mode"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
|
||||
<setting string="Allowed Extensions" help="List of allowed file extensions for attachments (comma-separated)">
|
||||
<div class="content-group">
|
||||
<div class="mt16">
|
||||
<field name="msg_allowed_extensions" class="o_light_label"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
</block>
|
||||
</app>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="mail.AttachmentList" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('o-mail-AttachmentCard-aside')]" position="before">
|
||||
<div class="o-mail-AttachmentCard-aside position-relative rounded-end overflow-hidden"
|
||||
t-if="isMsgFile(attachment)"
|
||||
t-att-class="{ 'o-hasMultipleActions d-flex flex-column': attachment.isDeletable and !env.inComposer }">
|
||||
<button t-on-click="() => this.openMsgViewer(attachment)"
|
||||
class="btn d-flex justify-content-center align-items-center w-100 h-100 rounded-0"
|
||||
t-attf-class="bg-300">
|
||||
<i class="fa fa-envelope-o" role="img" aria-label="Open in MSG Viewer" />
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
72
msg_viewer/static/src/components/msg_viewer/msg_viewer.js
Normal file
72
msg_viewer/static/src/components/msg_viewer/msg_viewer.js
Normal file
|
|
@ -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);
|
||||
16
msg_viewer/static/src/components/msg_viewer/msg_viewer.xml
Normal file
16
msg_viewer/static/src/components/msg_viewer/msg_viewer.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<!-- Template for the MSG viewer modal -->
|
||||
<t t-name="msg_viewer.Viewer">
|
||||
<Dialog title="'MSG Viewer'" size="'xl'">
|
||||
<div class="o_msg_viewer h-100">
|
||||
<div id="msg-viewer-container" class="h-100"></div>
|
||||
</div>
|
||||
<t t-set-slot="footer">
|
||||
<button class="btn btn-primary" t-on-click="() => this.action.doAction({ type: 'ir.actions.act_window_close' })">
|
||||
Close
|
||||
</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
50
msg_viewer/static/src/models/attachment_card_msg.js
Normal file
50
msg_viewer/static/src/models/attachment_card_msg.js
Normal file
|
|
@ -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");
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="web.MsgViewerField">
|
||||
<t t-if="!props.readonly">
|
||||
<div class="o_form_msg_controls d-flex gap-1">
|
||||
<t t-if="props.value">
|
||||
<FileUploader
|
||||
acceptedFileExtensions="'.msg'"
|
||||
onUploaded.bind="onFileUploaded"
|
||||
>
|
||||
<t t-set-slot="toggler">
|
||||
<button
|
||||
class="btn btn-secondary fa fa-pencil o_select_file_button"
|
||||
data-tooltip="Edit"
|
||||
aria-label="Edit"
|
||||
/>
|
||||
</t>
|
||||
<button
|
||||
class="btn btn-secondary fa fa-trash o_clear_file_button"
|
||||
data-tooltip="Clear"
|
||||
aria-label="Clear"
|
||||
t-on-click="onFileRemove"
|
||||
/>
|
||||
</FileUploader>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<label class="o_select_file_button btn btn-primary">
|
||||
<FileUploader
|
||||
acceptedFileExtensions="'.msg'"
|
||||
onUploaded.bind="onFileUploaded"
|
||||
>
|
||||
<t t-set-slot="toggler">
|
||||
Upload your file
|
||||
</t>
|
||||
</FileUploader>
|
||||
</label>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="props.value">
|
||||
<div class="o_field_msg_viewer_container o_field_widget o_field_binary o_field_binary_file cursor-pointer" t-on-click="openMsgViewer">
|
||||
<i class="fa fa-envelope"/> <span t-esc="props.value.name"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="state.showViewer">
|
||||
<div class="o_field_msg_viewer_container">
|
||||
<iframe t-att-src="url" class="o_field_msg_viewer" t-on-error="onLoadFailed"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -6,10 +6,26 @@
|
|||
<field name="model">ir.attachment</field>
|
||||
<field name="inherit_id" ref="base.view_attachment_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='url']" position="after">
|
||||
<xpath expr="//field[@name='datas']" position="after">
|
||||
<field name="is_msg_file" invisible="1"/>
|
||||
<field name="datas" widget="msg_viewer" invisible="not is_msg_file"/>
|
||||
<field name="datas" widget="msg_viewer"
|
||||
invisible="not is_msg_file"
|
||||
filename="name"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='datas']" position="attributes">
|
||||
<attribute name="invisible">is_msg_file</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_attachment_search_msg" model="ir.ui.view">
|
||||
<field name="name">ir.attachment.search.msg</field>
|
||||
<field name="model">ir.attachment</field>
|
||||
<field name="inherit_id" ref="base.view_attachment_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<filter name="url_filter" position="after">
|
||||
<filter string="MSG Files" name="msg_files" domain="[('is_msg_file', '=', True)]"/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
|
|||
Loading…
Reference in a new issue