msg_attachments: switch info logging to debug

This commit is contained in:
Marc Durepos 2025-01-22 15:21:00 -05:00
parent fabc7a6740
commit de44113336

View file

@ -16,33 +16,41 @@ from odoo.tools import email_normalize, email_split
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class IrAttachment(models.Model):
_inherit = 'ir.attachment'
msg_processed = fields.Boolean(string='MSG Processed', default=False) class IrAttachment(models.Model):
mail_message_id = fields.Many2one('mail.message', string='Created Mail Message') _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): def _is_msg_file(self):
"""Check if the attachment is an MSG file and validate size.""" """Check if the attachment is an MSG file and validate size."""
self.ensure_one() self.ensure_one()
# Check if it's an MSG file # Check if it's an MSG file
is_msg = ( is_msg = self.mimetype == "application/vnd.ms-outlook" or (
self.mimetype == 'application/vnd.ms-outlook' or self.name and self.name.lower().endswith(".msg")
(self.name and self.name.lower().endswith('.msg'))
) )
if not is_msg: if not is_msg:
return False return False
# Check file size # Check file size
max_size = int(self.env['ir.config_parameter'].sudo().get_param( max_size = int(
'msg_attachments.max_file_size', '25')) self.env["ir.config_parameter"]
file_size_mb = len(self.datas) * 3 / 4 / 1024 / 1024 # Convert from base64 to MB .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: if file_size_mb > max_size:
self._notify_error('File Size Error', self._notify_error(
_('The MSG file is too large. Maximum allowed size is %s MB') % max_size) "File Size Error",
_("The MSG file is too large. Maximum allowed size is %s MB")
% max_size,
)
return False return False
return True return True
@ -50,32 +58,46 @@ class IrAttachment(models.Model):
def _clean_header_value(self, value): def _clean_header_value(self, value):
"""Clean header value by removing line breaks and extra whitespace.""" """Clean header value by removing line breaks and extra whitespace."""
if not value: if not value:
return '' return ""
# Replace any combination of whitespace (including newlines) with a single space # Replace any combination of whitespace (including newlines) with a single space
return ' '.join(str(value).split()) return " ".join(str(value).split())
def _msg_to_eml(self, msg_data): def _msg_to_eml(self, msg_data):
"""Convert MSG file data to EML format.""" """Convert MSG file data to EML format."""
try: try:
# Read the MSG file # Read the MSG file
msg_file = extract_msg.Message(msg_data) msg_file = extract_msg.Message(msg_data)
_logger.info('Converting MSG to EML - Subject: %s, From: %s, To: %s', _logger.debug(
msg_file.subject, msg_file.sender, msg_file.to) "Converting MSG to EML - Subject: %s, From: %s, To: %s",
msg_file.subject,
msg_file.sender,
msg_file.to,
)
# Create email message # Create email message
email_msg = MIMEMultipart('related') email_msg = MIMEMultipart("related")
# Add headers # Add headers
email_msg['Subject'] = self._clean_header_value(msg_file.subject) or "No Subject" email_msg["Subject"] = (
email_msg['From'] = self._clean_header_value(msg_file.sender) self._clean_header_value(msg_file.subject) or "No Subject"
email_msg['To'] = self._clean_header_value(msg_file.to) )
email_msg['Cc'] = self._clean_header_value(msg_file.cc) if msg_file.cc else None email_msg["From"] = self._clean_header_value(msg_file.sender)
email_msg['Date'] = msg_file.date.strftime('%a, %d %b %Y %H:%M:%S %z') if msg_file.date else formatdate(localtime=True) email_msg["To"] = self._clean_header_value(msg_file.to)
email_msg["Cc"] = (
self._clean_header_value(msg_file.cc) if msg_file.cc else None
)
email_msg["Date"] = (
msg_file.date.strftime("%a, %d %b %Y %H:%M:%S %z")
if msg_file.date
else formatdate(localtime=True)
)
# Add original MSG headers if present # Add original MSG headers if present
for header in ['Message-ID', 'In-Reply-To', 'References']: for header in ["Message-ID", "In-Reply-To", "References"]:
if header.lower() in msg_file.header: if header.lower() in msg_file.header:
header_value = self._clean_header_value(msg_file.header[header.lower()]) header_value = self._clean_header_value(
msg_file.header[header.lower()]
)
if header_value: # Only add header if it has a value if header_value: # Only add header if it has a value
email_msg[header] = header_value email_msg[header] = header_value
@ -84,81 +106,108 @@ class IrAttachment(models.Model):
body = msg_file.body body = msg_file.body
# Si ce n'est pas déjà du HTML mais contient des CIDs, convertir en HTML # Si ce n'est pas déjà du HTML mais contient des CIDs, convertir en HTML
if '<html' not in body.lower() and '[cid:' in body: if "<html" not in body.lower() and "[cid:" in body:
# Convertir les retours à la ligne en <br> # Convertir les retours à la ligne en <br>
body = body.replace('\n', '<br>') body = body.replace("\n", "<br>")
# Convertir les références CID en balises img # Convertir les références CID en balises img
import re import re
body = re.sub( body = re.sub(
r'\[cid:([^\]]+)\]', r"\[cid:([^\]]+)\]",
lambda m: f'<img src="cid:{m.group(1)}" alt="{m.group(1)}">', lambda m: f'<img src="cid:{m.group(1)}" alt="{m.group(1)}">',
body body,
) )
# Envelopper dans du HTML # Envelopper dans du HTML
body = f'<html><body>{body}</body></html>' body = f"<html><body>{body}</body></html>"
# Clean HTML if needed # Clean HTML if needed
if '<html' in body.lower(): if "<html" in body.lower():
body = html_sanitize(body) body = html_sanitize(body)
html_part = MIMEText(body, 'html') html_part = MIMEText(body, "html")
email_msg.attach(html_part) email_msg.attach(html_part)
else: else:
html_part = MIMEText('', 'html') html_part = MIMEText("", "html")
email_msg.attach(html_part) email_msg.attach(html_part)
# Add attachments # Add attachments
for attachment in msg_file.attachments: for attachment in msg_file.attachments:
if not hasattr(attachment, 'data') or not attachment.data: if not hasattr(attachment, "data") or not attachment.data:
continue continue
try: try:
filename = attachment.getFilename() filename = attachment.getFilename()
except AttributeError: except AttributeError:
filename = 'unknown.bin' filename = "unknown.bin"
_logger.warning('Could not get filename for attachment, using default: %s', filename) _logger.warning(
"Could not get filename for attachment, using default: %s",
filename,
)
if hasattr(attachment, 'contentId') and attachment.contentId: if hasattr(attachment, "contentId") and attachment.contentId:
# Image avec CID - utiliser MIMEImage # Image avec CID - utiliser MIMEImage
maintype, subtype = (attachment.mimetype or 'image/jpeg').split('/', 1) maintype, subtype = (attachment.mimetype or "image/jpeg").split(
if maintype == 'image': "/", 1
)
if maintype == "image":
mime_part = MIMEImage(attachment.data, _subtype=subtype) mime_part = MIMEImage(attachment.data, _subtype=subtype)
mime_part.add_header('Content-ID', f"<{attachment.contentId.strip('<>')}>") mime_part.add_header(
mime_part.add_header('Content-Disposition', 'inline', filename=filename) "Content-ID", f"<{attachment.contentId.strip('<>')}>"
_logger.info('Adding inline image with CID: %s, filename: %s', attachment.contentId, filename) )
mime_part.add_header(
"Content-Disposition", "inline", filename=filename
)
_logger.debug(
"Adding inline image with CID: %s, filename: %s",
attachment.contentId,
filename,
)
else: else:
# Fallback pour les non-images avec CID # Fallback pour les non-images avec CID
mime_part = MIMEBase(maintype, subtype) mime_part = MIMEBase(maintype, subtype)
mime_part.set_payload(attachment.data) mime_part.set_payload(attachment.data)
encoders.encode_base64(mime_part) encoders.encode_base64(mime_part)
mime_part.add_header('Content-ID', f"<{attachment.contentId.strip('<>')}>") mime_part.add_header(
mime_part.add_header('Content-Disposition', 'inline', filename=filename) "Content-ID", f"<{attachment.contentId.strip('<>')}>"
)
mime_part.add_header(
"Content-Disposition", "inline", filename=filename
)
else: else:
# Pièce jointe normale # Pièce jointe normale
maintype, subtype = (attachment.mimetype or 'application/octet-stream').split('/', 1) maintype, subtype = (
attachment.mimetype or "application/octet-stream"
).split("/", 1)
mime_part = MIMEBase(maintype, subtype) mime_part = MIMEBase(maintype, subtype)
mime_part.set_payload(attachment.data) mime_part.set_payload(attachment.data)
encoders.encode_base64(mime_part) encoders.encode_base64(mime_part)
mime_part.add_header('Content-Disposition', 'attachment', filename=filename) mime_part.add_header(
"Content-Disposition", "attachment", filename=filename
)
email_msg.attach(mime_part) email_msg.attach(mime_part)
return email_msg.as_string() return email_msg.as_string()
except Exception as e: except Exception as e:
_logger.error('Error converting MSG to EML: %s', str(e), exc_info=True) _logger.error("Error converting MSG to EML: %s", str(e), exc_info=True)
raise raise
def process_msg_as_email(self): def process_msg_as_email(self):
"""Convert MSG file to EML and process it using Odoo's mail module.""" """Convert MSG file to EML and process it using Odoo's mail module."""
self.ensure_one() self.ensure_one()
_logger.info('Starting MSG processing for file: %s (model: %s, res_id: %s)', _logger.debug(
self.name, self.res_model, self.res_id) "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(): if not self._is_msg_file():
_logger.warning('File %s is not a valid MSG file', self.name) _logger.warning("File %s is not a valid MSG file", self.name)
self._notify_error('Invalid File', self._notify_error(
_('The selected file is not a valid MSG file')) "Invalid File", _("The selected file is not a valid MSG file")
)
return False return False
try: try:
@ -167,18 +216,20 @@ class IrAttachment(models.Model):
eml_content = self._msg_to_eml(msg_data) eml_content = self._msg_to_eml(msg_data)
# Process using Odoo's standard mail handling # Process using Odoo's standard mail handling
thread_model = self.env[self.res_model] if self.res_model else self.env['mail.thread'] thread_model = (
_logger.info('Using thread model: %s', thread_model._name) self.env[self.res_model] if self.res_model else self.env["mail.thread"]
)
_logger.debug("Using thread model: %s", thread_model._name)
# Set context for message creation # Set context for message creation
context = { context = {
'default_model': self.res_model, "default_model": self.res_model,
'default_res_id': self.res_id, "default_res_id": self.res_id,
'mail_create_nosubscribe': True, "mail_create_nosubscribe": True,
'mail_create_nolog': True, "mail_create_nolog": True,
'mail_notify_force_send': False, "mail_notify_force_send": False,
'mail_create_force_model': self.res_model, "mail_create_force_model": self.res_model,
'mail_thread_quote': False, "mail_thread_quote": False,
} }
# Process the EML using Odoo's standard message_process # Process the EML using Odoo's standard message_process
@ -186,39 +237,51 @@ class IrAttachment(models.Model):
model=self.res_model, model=self.res_model,
message=eml_content, message=eml_content,
custom_values={ custom_values={
'res_id': self.res_id, "res_id": self.res_id,
'model': self.res_model, "model": self.res_model,
'msg_attachment_id': self.id, "msg_attachment_id": self.id,
}, },
thread_id=self.res_id thread_id=self.res_id,
) )
if thread_id: if thread_id:
_logger.info('Message processed successfully, thread_id: %s', thread_id) _logger.debug(
"Message processed successfully, thread_id: %s", thread_id
)
# Mark MSG as processed # Mark MSG as processed
self.write({ self.write(
'description': _('Processed as email on %s') % fields.Datetime.now(), {
'msg_processed': True, "description": _("Processed as email on %s")
}) % fields.Datetime.now(),
"msg_processed": True,
}
)
return thread_id return thread_id
else: else:
_logger.error('message_process failed to create thread_id') _logger.error("message_process failed to create thread_id")
return False return False
except Exception as e: except Exception as e:
_logger.error('Error processing MSG file %s: %s', self.name, str(e), exc_info=True) _logger.error(
self._notify_error('Processing Error', "Error processing MSG file %s: %s", self.name, str(e), exc_info=True
_('Could not process the MSG file: %s') % str(e)) )
self._notify_error(
"Processing Error", _("Could not process the MSG file: %s") % str(e)
)
return False return False
def _notify_error(self, title, message): def _notify_error(self, title, message):
"""Show error notification to the user.""" """Show error notification to the user."""
self.env['bus.bus']._sendone(self.env.user.partner_id, 'notification', { self.env["bus.bus"]._sendone(
'type': 'danger', self.env.user.partner_id,
'title': title, "notification",
'message': message, {
'sticky': True, "type": "danger",
}) "title": title,
"message": message,
"sticky": True,
},
)
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):