msg_attachments_to_mail_message

This commit is contained in:
xtremxpert 2025-01-21 22:10:31 -05:00
parent 98b5ed31fd
commit aa4fbe72ec
18 changed files with 726 additions and 145 deletions

View file

@ -0,0 +1 @@
from . import models

View 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,
}

View file

@ -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 $$;
""")

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,2 @@
from . import ir_attachment
from . import res_config_settings

View 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

View file

@ -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'
)

View file

@ -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>

View file

@ -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,

View file

@ -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>

View 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);

View 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>

View file

@ -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;
}

View file

@ -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");

View 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");

View file

@ -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>

View file

@ -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>