more msg for nesting

This commit is contained in:
Benoît Vézina 2025-01-31 17:25:25 -05:00
parent 5aa0f5b519
commit 853c239deb
3 changed files with 284 additions and 133 deletions

View file

@ -6,28 +6,28 @@ attachments, handling headers, and creating proper mail.message records.
""" """
from odoo import models, api, fields from odoo import models, api, fields
from odoo.tools import html_sanitize from odoo.tools import html_sanitize, config
import base64
import logging
from odoo.tools.translate import _ from odoo.tools.translate import _
from odoo.tools import email_normalize, email_split
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.image import MIMEImage from email.mime.image import MIMEImage
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email import encoders from email import encoders
import re from email.utils import make_msgid, formatdate
from odoo.tools import email_normalize, email_split from email.header import Header
from io import BytesIO
import extract_msg import base64
import psycopg2 import logging
import datetime
import time
import os import os
import shutil import shutil
from odoo.tools import config import mimetypes
import time import psycopg2
import random import extract_msg
import socket from io import BytesIO
import tempfile
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -253,10 +253,7 @@ class IrAttachment(models.Model):
msg_file.to, msg_file.to,
) )
# Create email message # Clean and normalize email addresses for partner matching
email_msg = MIMEMultipart("related")
# Clean and normalize email addresses
from_email = email_normalize(msg_file.sender) if msg_file.sender else False from_email = email_normalize(msg_file.sender) if msg_file.sender else False
to_emails = email_split(msg_file.to) if msg_file.to else [] to_emails = email_split(msg_file.to) if msg_file.to else []
cc_emails = email_split(msg_file.cc) if msg_file.cc else [] cc_emails = email_split(msg_file.cc) if msg_file.cc else []
@ -271,7 +268,7 @@ class IrAttachment(models.Model):
except Exception as e: except Exception as e:
_logger.warning("Failed to get current partner: %s", str(e)) _logger.warning("Failed to get current partner: %s", str(e))
# Recherche du partenaire expéditeur avec gestion d'erreur # Find the sender partner with error handling
from_partner = None from_partner = None
if from_email: if from_email:
try: try:
@ -293,20 +290,80 @@ class IrAttachment(models.Model):
from_partner = company_contacts[0] from_partner = company_contacts[0]
except Exception as e: except Exception as e:
_logger.warning("Failed to process sender partner: %s", str(e)) _logger.warning("Failed to process sender partner: %s", str(e))
# Create email message
email_msg = MIMEMultipart('mixed')
# Set basic headers with proper encoding
email_msg['Subject'] = self._encode_header_content(msg_file.subject)
email_msg['From'] = self._encode_header_content(msg_file.sender)
email_msg['To'] = self._encode_header_content(msg_file.to)
# Format the date properly for email header
if msg_file.date:
if isinstance(msg_file.date, datetime.datetime):
timestamp = msg_file.date.timestamp()
else:
timestamp = time.time()
email_msg['Date'] = formatdate(timestamp, localtime=True)
else:
email_msg['Date'] = formatdate(time.time(), localtime=True)
# Generate a new Message-ID if not present
try:
message_id = msg_file.message_id
except AttributeError:
message_id = make_msgid()
email_msg['Message-ID'] = message_id
# Set email headers # Handle CC if present
email_msg["Subject"] = self._clean_header_value(msg_file.subject)
email_msg["From"] = self._clean_header_value(msg_file.sender)
email_msg["To"] = self._clean_header_value(msg_file.to)
if msg_file.cc: if msg_file.cc:
email_msg["Cc"] = self._clean_header_value(msg_file.cc) email_msg['CC'] = self._encode_header_content(msg_file.cc)
# Add email body # Create the message body
body_part = MIMEMultipart('alternative')
# Add HTML version if available
if msg_file.htmlBody:
_logger.info("Adding HTML body")
# Detect encoding and decode properly
html_content = msg_file.htmlBody
if isinstance(html_content, bytes):
encoding = self._detect_encoding(html_content)
try:
html_content = html_content.decode(encoding)
except UnicodeDecodeError:
html_content = html_content.decode('utf-8', errors='replace')
html_part = MIMEText(html_content, 'html', 'utf-8')
body_part.attach(html_part)
# Add RTF version if available and no HTML
elif msg_file.rtfBody:
_logger.info("Adding RTF body")
rtf_content = msg_file.rtfBody
if isinstance(rtf_content, bytes):
encoding = self._detect_encoding(rtf_content)
try:
rtf_content = rtf_content.decode(encoding)
except UnicodeDecodeError:
rtf_content = rtf_content.decode('utf-8', errors='replace')
rtf_part = MIMEText(rtf_content, 'rtf', 'utf-8')
body_part.attach(rtf_part)
# Fallback to plain text
if msg_file.body: if msg_file.body:
body = html_sanitize(msg_file.body) _logger.info("Adding plain text body")
email_msg.attach(MIMEText(body, "html")) text_content = msg_file.body
if isinstance(text_content, bytes):
encoding = self._detect_encoding(text_content)
try:
text_content = text_content.decode(encoding)
except UnicodeDecodeError:
text_content = text_content.decode('utf-8', errors='replace')
text_part = MIMEText(text_content, 'plain', 'utf-8')
body_part.attach(text_part)
email_msg.attach(body_part)
# Process attachments # Process attachments with special handling for inline images
for att in msg_file.attachments: for att in msg_file.attachments:
try: try:
filename = att.longFilename or att.shortFilename filename = att.longFilename or att.shortFilename
@ -315,118 +372,144 @@ class IrAttachment(models.Model):
continue continue
_logger.info("=== Processing attachment: %s ===", filename) _logger.info("=== Processing attachment: %s ===", filename)
_logger.info("Attachment type: %s", type(att.data).__name__)
_logger.info("Attachment size: %d bytes", len(att.data) if hasattr(att.data, '__len__') else -1)
# Check if attachment is an email file (MSG or EML)
if isinstance(att.data, extract_msg.msg_classes.message.Message):
_logger.info("Processing as MSG file: %s", filename)
_logger.info("MSG attributes: %s", dir(att.data))
mime_type = "application/vnd.ms-outlook"
elif filename.lower().endswith('.eml'):
_logger.info("Processing as EML file: %s", filename)
mime_type = "message/rfc822"
else:
# Regular attachment
_logger.info("Processing as regular attachment: %s", filename)
_logger.info("Content type: %s", getattr(att, 'content_type', 'unknown'))
part = MIMEBase("application", "octet-stream")
part.set_payload(att.data)
encoders.encode_base64(part)
part.add_header(
"Content-Disposition",
f"attachment; filename={filename}",
)
email_msg.attach(part)
_logger.info("Regular attachment processed successfully")
continue
# Process email file (MSG or EML)
_logger.info("Creating MIME part with type: %s", mime_type)
maintype, subtype = mime_type.split('/')
part = MIMEBase(maintype, subtype)
# Get the raw data # Check if it's an inline image
if isinstance(att.data, extract_msg.msg_classes.message.Message): if hasattr(att, 'cid') and att.cid:
_logger.info("Extracting raw bytes from MSG") # This is an inline image
_logger.info("Processing inline image with CID: %s", att.cid)
try: try:
# Try to get raw data using asEmailMessage # Detect MIME type based on filename
_logger.info("Converting MSG to email message") mime_type, _ = mimetypes.guess_type(filename)
email_message = att.data.asEmailMessage() if mime_type and mime_type.startswith('image/'):
raw_data = email_message.as_bytes() image_part = MIMEImage(att.data, _subtype=mime_type.split('/')[-1])
_logger.info("Successfully converted MSG to email message: %d bytes", len(raw_data)) else:
# Fallback to application/octet-stream for unknown types
part = MIMEBase("application", "octet-stream")
part.set_payload(att.data)
encoders.encode_base64(part)
part.add_header('Content-ID', f'<{att.cid}>')
part.add_header('Content-Disposition', 'inline', filename=filename)
email_msg.attach(part)
continue
image_part.add_header('Content-ID', f'<{att.cid}>')
image_part.add_header('Content-Disposition', 'inline', filename=filename)
email_msg.attach(image_part)
continue
except Exception as e: except Exception as e:
_logger.warning("Failed to convert MSG to email message: %s", str(e)) _logger.warning("Failed to process inline image: %s", str(e))
try:
# Try to export as bytes
_logger.info("Trying to export MSG as bytes")
raw_data = att.data.exportBytes()
_logger.info("Successfully exported MSG as bytes: %d bytes", len(raw_data))
except Exception as e:
_logger.warning("Failed to export MSG as bytes: %s", str(e))
# Last resort: try to get the raw data directly
if hasattr(att.data, '_raw'):
_logger.info("Using _raw attribute")
raw_data = att.data._raw
else:
_logger.warning("Could not get raw data from MSG, skipping attachment")
continue
else:
_logger.info("Using raw data directly for EML")
raw_data = att.data
if raw_data: # Regular attachment processing
_logger.info("Raw data size: %d bytes", len(raw_data) if raw_data else 0) _logger.info("Processing as regular attachment: %s", filename)
part = MIMEBase("application", "octet-stream")
# Check if the data is base64 encoded part.set_payload(att.data)
try: encoders.encode_base64(part)
# Try to decode base64 if it looks like base64 part.add_header(
if isinstance(raw_data, str) and all(c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' for c in raw_data): "Content-Disposition",
_logger.info("Data appears to be base64 encoded, decoding...") f"attachment; filename={filename}",
import base64
raw_data = base64.b64decode(raw_data)
_logger.info("Successfully decoded base64 data: %d bytes", len(raw_data))
except Exception as e:
_logger.warning("Failed to decode base64 data: %s", str(e))
# Set the payload and encode
part.set_payload(raw_data)
_logger.info("Encoding attachment in base64")
encoders.encode_base64(part)
# Add headers
_logger.info("Adding headers to MIME part")
part.add_header(
"Content-Disposition",
f"attachment; filename={filename}",
)
part.add_header(
"Content-Type",
mime_type,
name=filename
)
_logger.info("Attaching processed email file to message")
email_msg.attach(part)
_logger.info("=== Finished processing %s ===", filename)
else:
_logger.warning("No raw data available for %s, skipping", filename)
except Exception as e:
_logger.warning(
"Failed to process attachment %s: %s - Attachment data type: %s, Content: %s",
filename,
str(e),
type(att.data),
str(att.data)[:100] if att.data else "None",
exc_info=True
) )
return email_msg.as_string() email_msg.attach(part)
_logger.info("Regular attachment processed successfully")
except Exception as e:
_logger.warning("Failed to process attachment %s: %s",
filename if 'filename' in locals() else 'Unknown',
str(e))
continue
# Prepare custom values for message processing
custom_values = {}
if from_partner:
custom_values['author_id'] = from_partner.id
# If this is related to a sale, add the sale thread
if self.res_model == 'sale.order' and self.res_id:
custom_values['model'] = 'sale.order'
custom_values['res_id'] = self.res_id
# Return the complete email message as string
try:
eml_content = email_msg.as_string()
# Determine the target model and thread_id
target_model = self.res_model or 'mail.thread'
target_thread_id = self.res_id if self.res_model else False
# Process the email with forced model
self.env['mail.thread'].with_context(
mail_create_nosubscribe=True, # Don't auto-subscribe the sender
mail_create_nolog=True, # Don't create log message
default_model=target_model, # Force the model
).message_process(
model=target_model,
message=eml_content,
custom_values=custom_values,
save_original=True,
strip_attachments=False,
thread_id=target_thread_id
)
# Return the EML content instead of True
return eml_content
except Exception as e:
_logger.error("Error converting email message to string: %s", str(e))
raise
except Exception as e: except Exception as e:
_logger.error("Error converting MSG to EML: %s", str(e)) _logger.error("Error converting MSG to EML: %s", str(e))
raise raise
def _encode_header_content(self, content):
"""
Encode header content properly to handle special characters.
Args:
content (str): Content to encode
Returns:
str: Encoded content
"""
if not content:
return ""
if isinstance(content, bytes):
encoding = self._detect_encoding(content)
try:
content = content.decode(encoding)
except UnicodeDecodeError:
content = content.decode('utf-8', errors='replace')
from email.header import Header
return str(Header(content, 'utf-8'))
def _ensure_html_content(self, content, is_html=False):
"""
Ensure content is in HTML format and properly formatted.
Args:
content (str): The content to format
is_html (bool): Whether the content is already HTML
Returns:
str: HTML formatted content
"""
if not content:
return ""
# If content is already HTML, just sanitize it
if is_html or bool(re.search(r'<[^>]+>', content)):
return html_sanitize(content)
# Convert plain text to HTML
content = content.replace('&', '&amp;')
content = content.replace('<', '&lt;')
content = content.replace('>', '&gt;')
content = content.replace('\n', '<br/>')
# Wrap in HTML tags
html_content = f"<div style='font-family: Arial, sans-serif;'>{content}</div>"
return html_sanitize(html_content)
def _get_review_dir(self, error_type): def _get_review_dir(self, error_type):
""" """
Get the review directory path. Get the review directory path.
@ -499,7 +582,7 @@ class IrAttachment(models.Model):
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.
Returns: Returns:
bool: True if successful, False otherwise str: EML content if successful, False otherwise
""" """
try: try:
self.ensure_one() self.ensure_one()
@ -583,12 +666,12 @@ class IrAttachment(models.Model):
"msg_processed": True, "msg_processed": True,
} }
) )
return thread_id return eml_content
except psycopg2.IntegrityError as e: except psycopg2.IntegrityError as e:
if "mail_followers_mail_followers_res_partner_res_model_id_uniq" in str(e): if "mail_followers_mail_followers_res_partner_res_model_id_uniq" in str(e):
_logger.warning("Follower already exists, skipping: %s", str(e)) _logger.warning("Follower already exists, skipping: %s", str(e))
return thread_id if 'thread_id' in locals() else False return eml_content if 'eml_content' in locals() else False
raise raise
except Exception as e: except Exception as e:

Binary file not shown.

View file

@ -0,0 +1,68 @@
import extract_msg
import os
import base64
def analyze_msg(file_path):
print(f"Analyzing MSG file: {file_path}")
msg = extract_msg.Message(file_path)
print("\n=== Basic Information ===")
print(f"Subject: {msg.subject}")
print(f"From: {msg.sender}")
print(f"To: {msg.to}")
print("\n=== Body Properties ===")
print("Has RTF:", hasattr(msg, '_rtf'))
print("\nPlain Body Type:", type(msg.body))
print("Plain Body Preview:")
print("---START OF PLAIN BODY---")
print(msg.body[:500])
print("---END OF PLAIN BODY---")
print("\nHTML Body Preview:")
print("---START OF HTML BODY---")
if msg.htmlBody:
try:
html_content = msg.htmlBody.decode('utf-8', errors='ignore')
print(html_content[:1000])
except Exception as e:
print(f"Error decoding HTML body: {e}")
else:
print("No HTML body found")
print("---END OF HTML BODY---")
print("\nRTF Body Preview:")
print("---START OF RTF BODY---")
if msg.rtfBody:
try:
rtf_content = msg.rtfBody.decode('utf-8', errors='ignore')
print(rtf_content[:1000])
except Exception as e:
print(f"Error decoding RTF body: {e}")
else:
print("No RTF body found")
print("---END OF RTF BODY---")
print("\n=== Images ===")
for att in msg.attachments:
if att.longFilename and 'image' in att.longFilename.lower():
print(f"Found image: {att.longFilename}")
print(f"Content-ID: {att.cid if hasattr(att, 'cid') else 'No CID'}")
print("\n=== Available Attributes ===")
for attr in dir(msg):
if not attr.startswith('_'):
try:
value = getattr(msg, attr)
if not callable(value):
print(f"{attr}: {type(value)}")
except Exception as e:
print(f"{attr}: Error accessing - {str(e)}")
if __name__ == "__main__":
file_path = "FACTURE NO. 7005451.msg"
if os.path.exists(file_path):
analyze_msg(file_path)
else:
print(f"File not found: {file_path}")