more msg for nesting
This commit is contained in:
parent
5aa0f5b519
commit
853c239deb
3 changed files with 284 additions and 133 deletions
|
|
@ -6,28 +6,28 @@ attachments, handling headers, and creating proper mail.message records.
|
|||
"""
|
||||
|
||||
from odoo import models, api, fields
|
||||
from odoo.tools import html_sanitize
|
||||
import base64
|
||||
import logging
|
||||
from odoo.tools import html_sanitize, config
|
||||
from odoo.tools.translate import _
|
||||
from odoo.tools import email_normalize, email_split
|
||||
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.image import MIMEImage
|
||||
from email.mime.base import MIMEBase
|
||||
from email import encoders
|
||||
import re
|
||||
from odoo.tools import email_normalize, email_split
|
||||
from io import BytesIO
|
||||
import extract_msg
|
||||
import psycopg2
|
||||
from email.utils import make_msgid, formatdate
|
||||
from email.header import Header
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import datetime
|
||||
import time
|
||||
import os
|
||||
import shutil
|
||||
from odoo.tools import config
|
||||
import time
|
||||
import random
|
||||
import socket
|
||||
import tempfile
|
||||
|
||||
import mimetypes
|
||||
import psycopg2
|
||||
import extract_msg
|
||||
from io import BytesIO
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -253,10 +253,7 @@ class IrAttachment(models.Model):
|
|||
msg_file.to,
|
||||
)
|
||||
|
||||
# Create email message
|
||||
email_msg = MIMEMultipart("related")
|
||||
|
||||
# Clean and normalize email addresses
|
||||
# Clean and normalize email addresses for partner matching
|
||||
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 []
|
||||
cc_emails = email_split(msg_file.cc) if msg_file.cc else []
|
||||
|
|
@ -271,7 +268,7 @@ class IrAttachment(models.Model):
|
|||
except Exception as 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
|
||||
if from_email:
|
||||
try:
|
||||
|
|
@ -293,20 +290,80 @@ class IrAttachment(models.Model):
|
|||
from_partner = company_contacts[0]
|
||||
except Exception as 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
|
||||
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)
|
||||
# Handle CC if present
|
||||
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:
|
||||
body = html_sanitize(msg_file.body)
|
||||
email_msg.attach(MIMEText(body, "html"))
|
||||
_logger.info("Adding plain text body")
|
||||
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:
|
||||
try:
|
||||
filename = att.longFilename or att.shortFilename
|
||||
|
|
@ -315,118 +372,144 @@ class IrAttachment(models.Model):
|
|||
continue
|
||||
|
||||
_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
|
||||
if isinstance(att.data, extract_msg.msg_classes.message.Message):
|
||||
_logger.info("Extracting raw bytes from MSG")
|
||||
# Check if it's an inline image
|
||||
if hasattr(att, 'cid') and att.cid:
|
||||
# This is an inline image
|
||||
_logger.info("Processing inline image with CID: %s", att.cid)
|
||||
try:
|
||||
# Try to get raw data using asEmailMessage
|
||||
_logger.info("Converting MSG to email message")
|
||||
email_message = att.data.asEmailMessage()
|
||||
raw_data = email_message.as_bytes()
|
||||
_logger.info("Successfully converted MSG to email message: %d bytes", len(raw_data))
|
||||
# Detect MIME type based on filename
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
if mime_type and mime_type.startswith('image/'):
|
||||
image_part = MIMEImage(att.data, _subtype=mime_type.split('/')[-1])
|
||||
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:
|
||||
_logger.warning("Failed to convert MSG to email message: %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
|
||||
_logger.warning("Failed to process inline image: %s", str(e))
|
||||
|
||||
if raw_data:
|
||||
_logger.info("Raw data size: %d bytes", len(raw_data) if raw_data else 0)
|
||||
|
||||
# Check if the data is base64 encoded
|
||||
try:
|
||||
# Try to decode base64 if it looks like base64
|
||||
if isinstance(raw_data, str) and all(c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' for c in raw_data):
|
||||
_logger.info("Data appears to be base64 encoded, decoding...")
|
||||
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
|
||||
# Regular attachment processing
|
||||
_logger.info("Processing as regular attachment: %s", filename)
|
||||
part = MIMEBase("application", "octet-stream")
|
||||
part.set_payload(att.data)
|
||||
encoders.encode_base64(part)
|
||||
part.add_header(
|
||||
"Content-Disposition",
|
||||
f"attachment; filename={filename}",
|
||||
)
|
||||
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:
|
||||
_logger.error("Error converting MSG to EML: %s", str(e))
|
||||
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('&', '&')
|
||||
content = content.replace('<', '<')
|
||||
content = content.replace('>', '>')
|
||||
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):
|
||||
"""
|
||||
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.
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
str: EML content if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.ensure_one()
|
||||
|
|
@ -583,12 +666,12 @@ class IrAttachment(models.Model):
|
|||
"msg_processed": True,
|
||||
}
|
||||
)
|
||||
return thread_id
|
||||
return eml_content
|
||||
|
||||
except psycopg2.IntegrityError as e:
|
||||
if "mail_followers_mail_followers_res_partner_res_model_id_uniq" in 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
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
BIN
msg_viewer/test/FACTURE NO. 7005451.msg
Normal file
BIN
msg_viewer/test/FACTURE NO. 7005451.msg
Normal file
Binary file not shown.
68
msg_viewer/test/analyze_msg.py
Normal file
68
msg_viewer/test/analyze_msg.py
Normal 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}")
|
||||
Loading…
Reference in a new issue