845 lines
33 KiB
Python
845 lines
33 KiB
Python
"""This module extends ir.attachment model to handle MSG file attachments.
|
|
|
|
It provides functionality to convert MSG files (Outlook messages) to EML format
|
|
and process them as regular email messages in Odoo. This includes extracting
|
|
attachments, handling headers, and creating proper mail.message records.
|
|
"""
|
|
|
|
from odoo import models, api, fields
|
|
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
|
|
from email.utils import make_msgid, formatdate
|
|
from email.header import Header
|
|
|
|
import base64
|
|
import logging
|
|
import datetime
|
|
import time
|
|
import os
|
|
import shutil
|
|
import mimetypes
|
|
import psycopg2
|
|
import extract_msg
|
|
from io import BytesIO
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
_msg_import_logger = logging.getLogger("msg.import")
|
|
|
|
# BV: THIS IS FOR REMOVING ERROR IN DEV
|
|
# DO WE STILL NEED IT
|
|
#handler = logging.FileHandler("/var/log/odoo/msg_import.log")
|
|
#formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
#handler.setFormatter(formatter)
|
|
#_msg_import_logger.addHandler(handler)
|
|
#_msg_import_logger.setLevel(logging.ERROR)
|
|
|
|
class IrAttachment(models.Model):
|
|
_inherit = "ir.attachment"
|
|
|
|
# Cache au niveau de la classe
|
|
_msg_conversion_cache = {}
|
|
|
|
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.
|
|
|
|
Returns:
|
|
bool: True if the attachment is an MSG file, False otherwise.
|
|
"""
|
|
self.ensure_one()
|
|
|
|
# Check if it's an MSG file
|
|
is_msg = self.with_context().sudo().mimetype in ['application/vnd.ms-outlook', 'application/x-ole-storage'] 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") or 25
|
|
)
|
|
file_size_mb = (
|
|
len(self.datas) * 3 / 4 / 1024 / 1024
|
|
) # Convert from base64 to MB
|
|
|
|
if file_size_mb > max_size:
|
|
log_errors = self.env['ir.config_parameter'].sudo().get_param('msg_attachments.log_msg_error', 'False').lower() == 'true'
|
|
log_file = self.env['ir.config_parameter'].sudo().get_param('msg_attachments.log_file_path')
|
|
error_msg = f"File size error for {self.name}: File size ({file_size_mb:.2f} MB) exceeds maximum allowed size ({max_size} MB)"
|
|
if log_errors and log_file:
|
|
_msg_import_logger.error(error_msg)
|
|
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.
|
|
|
|
Args:
|
|
value (str): Header value to clean.
|
|
|
|
Returns:
|
|
str: Cleaned header value.
|
|
"""
|
|
if not value:
|
|
return ""
|
|
# Replace any combination of whitespace (including newlines) with a single space
|
|
return " ".join(str(value).split())
|
|
|
|
def _detect_encoding(self, data):
|
|
"""
|
|
Detect the encoding of the data.
|
|
|
|
Args:
|
|
data (bytes): Data to analyze
|
|
|
|
Returns:
|
|
str: Detected encoding (e.g., 'utf-8', 'iso-8859-1', etc.)
|
|
"""
|
|
try:
|
|
import chardet
|
|
result = chardet.detect(data)
|
|
encoding = result['encoding'] if result and result['encoding'] else 'utf-8'
|
|
_logger.debug("Detected encoding: %s with confidence: %s",
|
|
encoding, result.get('confidence', 0))
|
|
return encoding
|
|
except ImportError:
|
|
_logger.info("chardet not installed, defaulting to utf-8")
|
|
return 'utf-8'
|
|
except Exception as e:
|
|
_logger.warning("Error detecting encoding: %s", str(e))
|
|
return 'utf-8'
|
|
|
|
def _retry_operation(self, operation, max_retries=3, delay=1):
|
|
"""
|
|
Retry an operation with exponential backoff.
|
|
|
|
Args:
|
|
operation (callable): Function to retry
|
|
max_retries (int): Maximum number of retry attempts
|
|
delay (int): Initial delay between retries in seconds
|
|
|
|
Returns:
|
|
Any: Result of the operation
|
|
"""
|
|
last_error = None
|
|
for attempt in range(max_retries):
|
|
try:
|
|
return operation()
|
|
except Exception as e:
|
|
last_error = e
|
|
if attempt < max_retries - 1:
|
|
sleep_time = delay * (2 ** attempt) # Exponential backoff
|
|
_logger.warning(
|
|
"Operation failed (attempt %d/%d): %s. Retrying in %d seconds...",
|
|
attempt + 1, max_retries, str(e), sleep_time
|
|
)
|
|
time.sleep(sleep_time)
|
|
else:
|
|
_logger.error(
|
|
"Operation failed after %d attempts: %s",
|
|
max_retries, str(e)
|
|
)
|
|
raise last_error
|
|
|
|
def _get_cache_key(self, data):
|
|
"""
|
|
Generate a cache key for the data.
|
|
|
|
Args:
|
|
data (bytes): Data to generate key for
|
|
|
|
Returns:
|
|
str: Cache key
|
|
"""
|
|
import hashlib
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
@api.model
|
|
def _get_conversion_cache(self):
|
|
"""
|
|
Get the conversion cache.
|
|
|
|
Returns:
|
|
dict: Conversion cache
|
|
"""
|
|
return self._msg_conversion_cache
|
|
|
|
def _cached_msg_to_eml(self, msg_data):
|
|
"""
|
|
Cached version of MSG to EML conversion.
|
|
|
|
Args:
|
|
msg_data (bytes): MSG file data
|
|
|
|
Returns:
|
|
str: EML file data
|
|
"""
|
|
cache = self._get_conversion_cache()
|
|
cache_key = self._get_cache_key(msg_data)
|
|
|
|
if cache_key in cache:
|
|
_logger.debug("Using cached conversion result")
|
|
return cache[cache_key]
|
|
|
|
result = self._msg_to_eml(msg_data)
|
|
cache[cache_key] = result
|
|
return result
|
|
|
|
def _msg_to_eml(self, msg_data):
|
|
"""
|
|
Convert MSG file data to EML format.
|
|
|
|
Args:
|
|
msg_data (bytes): MSG file data.
|
|
|
|
Returns:
|
|
str: EML file data.
|
|
"""
|
|
try:
|
|
# Debug logging avant conversion
|
|
_logger.info("=== DEBUG INFO AVANT CONVERSION [%s] ===", self.name)
|
|
_logger.info("[%s] Type de msg_data: %s", self.name, type(msg_data))
|
|
_logger.info("[%s] Attributs disponibles: %s", self.name,
|
|
dir(msg_data) if hasattr(msg_data, '__dict__') else 'No attributes')
|
|
|
|
# Ensure we have bytes
|
|
if not isinstance(msg_data, bytes):
|
|
_logger.info("[%s] Conversion nécessaire", self.name)
|
|
if hasattr(msg_data, 'read'):
|
|
_logger.info("[%s] Conversion via read()", self.name)
|
|
msg_data = msg_data.read()
|
|
elif isinstance(msg_data, str):
|
|
_logger.info("[%s] Conversion string vers bytes", self.name)
|
|
msg_data = msg_data.encode('utf-8')
|
|
elif hasattr(msg_data, 'datas'):
|
|
_logger.info("[%s] Utilisation de l'attribut datas", self.name)
|
|
msg_data = base64.b64decode(msg_data.datas)
|
|
else:
|
|
error_msg = f"Expected bytes-like object, got {type(msg_data)}"
|
|
_logger.error("[%s] %s", self.name, error_msg)
|
|
self._move_to_review_dir('msg_conversion')
|
|
raise ValueError(error_msg)
|
|
|
|
# Debug logging après conversion
|
|
_logger.info("=== DEBUG INFO APRÈS CONVERSION [%s] ===", self.name)
|
|
_logger.info("[%s] Type final de msg_data: %s", self.name, type(msg_data))
|
|
_logger.info("[%s] Taille des données: %d bytes", self.name, len(msg_data))
|
|
|
|
# Read the MSG file
|
|
msg_file = extract_msg.Message(BytesIO(msg_data))
|
|
_logger.debug(
|
|
"Converting MSG to EML - Subject: %s, From: %s, To: %s",
|
|
msg_file.subject,
|
|
msg_file.sender,
|
|
msg_file.to,
|
|
)
|
|
|
|
# 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 []
|
|
|
|
# Get current record's partner if exists
|
|
current_partner = False
|
|
if self.res_model and self.res_id:
|
|
try:
|
|
record = self.env[self.res_model].browse(self.res_id)
|
|
if hasattr(record, "partner_id"):
|
|
current_partner = record.partner_id
|
|
except Exception as e:
|
|
_logger.warning("Failed to get current partner: %s", str(e))
|
|
|
|
# Find the sender partner with error handling
|
|
from_partner = None
|
|
if from_email:
|
|
try:
|
|
with self.env.cr.savepoint():
|
|
from_partners = self.env["res.partner"].search([
|
|
("email", "=ilike", from_email),
|
|
("active", "in", [True, False])
|
|
])
|
|
|
|
if len(from_partners) == 1:
|
|
from_partner = from_partners[0]
|
|
elif len(from_partners) > 1 and current_partner:
|
|
# Search among contacts linked to the current object's company
|
|
company = current_partner.commercial_partner_id
|
|
company_contacts = from_partners.filtered(
|
|
lambda p: p.commercial_partner_id == company
|
|
)
|
|
if company_contacts:
|
|
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
|
|
|
|
# Handle CC if present
|
|
if msg_file.cc:
|
|
email_msg['CC'] = self._encode_header_content(msg_file.cc)
|
|
|
|
# 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:
|
|
_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 with special handling for inline images
|
|
for att in msg_file.attachments:
|
|
try:
|
|
filename = att.longFilename or att.shortFilename
|
|
if not filename:
|
|
_logger.info("Skipping attachment with no filename")
|
|
continue
|
|
|
|
_logger.info("=== Processing attachment: %s ===", filename)
|
|
|
|
# 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:
|
|
# 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 process inline image: %s", str(e))
|
|
|
|
# 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}",
|
|
)
|
|
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
|
|
context = {
|
|
'default_model': target_model, # Force the model
|
|
'mail_notify_force_send': False, # Don't force send notifications
|
|
'mail_auto_subscribe_no_notify': True, # Prevent auto-subscription notifications
|
|
'is_msg_import': True, # Flag to identify MSG import
|
|
}
|
|
|
|
self.env['mail.thread'].with_context(**context).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 _convert_body_to_html(self, body, content_type):
|
|
"""
|
|
Convert the message body to HTML based on its content type.
|
|
|
|
Args:
|
|
body (str): The message body.
|
|
content_type (str): The content type of the body (e.g., 'text/plain', 'text/html', 'text/rtf').
|
|
|
|
Returns:
|
|
str: The message body converted to HTML.
|
|
"""
|
|
if content_type == 'text/plain':
|
|
# Convert plain text to HTML
|
|
return f"<pre>{body}</pre>"
|
|
elif content_type == 'text/rtf':
|
|
# Convert RTF to HTML (simplified example)
|
|
# Note: A proper RTF to HTML converter would be needed for production use
|
|
return f"<div>{body}</div>"
|
|
elif content_type == 'text/html':
|
|
# Return HTML as is
|
|
return body
|
|
else:
|
|
# Default to plain text conversion for unknown types
|
|
return f"<pre>{body}</pre>"
|
|
|
|
def _get_review_dir(self, error_type):
|
|
"""
|
|
Get the review directory path.
|
|
|
|
Args:
|
|
error_type (str): Type of error encountered
|
|
|
|
Returns:
|
|
str: Path to the review directory
|
|
"""
|
|
# Get Odoo's data directory
|
|
data_dir = config.get('data_dir', os.path.dirname(self._full_path(self.store_fname)))
|
|
|
|
# Create a specific directory for review files
|
|
review_base = os.path.join(data_dir, 'review_files')
|
|
|
|
# Create subdirectory for specific error type
|
|
review_dir = os.path.join(review_base, error_type)
|
|
|
|
return review_dir
|
|
|
|
def _move_to_review_dir(self, error_type):
|
|
"""
|
|
Move problematic files to a review directory.
|
|
|
|
Args:
|
|
error_type (str): Type of error encountered (e.g., 'pdf_page_count', 'msg_conversion')
|
|
"""
|
|
self.ensure_one()
|
|
try:
|
|
if not self.store_fname:
|
|
_logger.warning("[%s] No stored file to move", self.name or 'Unknown')
|
|
return
|
|
|
|
# Get review directory
|
|
review_dir = self._get_review_dir(error_type)
|
|
os.makedirs(review_dir, exist_ok=True)
|
|
|
|
# Move file to review directory
|
|
src_path = self._full_path(self.store_fname)
|
|
if os.path.exists(src_path):
|
|
# Create a subdirectory for the current date
|
|
date_dir = os.path.join(review_dir, fields.Date.today().strftime('%Y%m%d'))
|
|
os.makedirs(date_dir, exist_ok=True)
|
|
|
|
# Add timestamp to filename to avoid conflicts
|
|
timestamp = fields.Datetime.now().strftime('%H%M%S')
|
|
filename = f"{timestamp}_{self.id}_{self.name}"
|
|
dst_path = os.path.join(date_dir, filename)
|
|
|
|
# Copy file instead of moving to preserve original
|
|
shutil.copy2(src_path, dst_path)
|
|
|
|
# Log the action
|
|
_logger.info(
|
|
"File %s moved to review directory due to %s error. New location: %s",
|
|
self.name or 'Unknown', error_type, dst_path
|
|
)
|
|
|
|
# Update attachment record
|
|
self.write({
|
|
'description': f"{self.description or ''}\nMoved to review directory due to {error_type} error on {fields.Datetime.now()}"
|
|
})
|
|
|
|
except Exception as e:
|
|
_logger.error("[%s] Failed to move file to review directory: %s", self.name or 'Unknown', str(e))
|
|
|
|
def process_msg_as_email(self):
|
|
"""
|
|
Convert MSG file to EML and process it using Odoo's mail module.
|
|
|
|
Returns:
|
|
str: EML content if successful, False otherwise
|
|
"""
|
|
try:
|
|
self.ensure_one()
|
|
|
|
if not self._is_msg_file():
|
|
_logger.warning("File is not a valid MSG file: %s", self.name)
|
|
return False
|
|
|
|
if not self.datas:
|
|
_logger.warning("No data found in MSG file: %s", self.name)
|
|
return False
|
|
|
|
if self.msg_processed:
|
|
_logger.debug("MSG file already processed: %s", self.name)
|
|
return True
|
|
|
|
def convert_msg():
|
|
msg_data = base64.b64decode(self.datas)
|
|
if not isinstance(msg_data, bytes):
|
|
raise ValueError(f"Expected bytes after b64decode, got {type(msg_data)}")
|
|
|
|
# Use cached conversion
|
|
eml_content = self._cached_msg_to_eml(msg_data)
|
|
if not isinstance(eml_content, str):
|
|
raise ValueError(f"Expected string EML content, got {type(eml_content)}")
|
|
return eml_content
|
|
|
|
try:
|
|
# Use retry mechanism for conversion
|
|
eml_content = self._retry_operation(convert_msg)
|
|
except Exception as e:
|
|
_logger.error("Data conversion error for %s: %s", self.name, str(e))
|
|
return False
|
|
|
|
if not eml_content:
|
|
_logger.error("Failed to convert MSG to EML: %s", self.name)
|
|
return False
|
|
|
|
# Convert the body to HTML
|
|
eml_content = self._convert_body_to_html(eml_content, 'text/plain')
|
|
|
|
# Get thread model based on res_model
|
|
thread_model = (
|
|
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
|
|
context = {
|
|
'default_model': self.res_model,
|
|
'default_res_id': self.res_id,
|
|
'mail_notify_force_send': False, # Don't force send notifications
|
|
'mail_auto_subscribe_no_notify': True, # Prevent auto-subscription notifications
|
|
'is_msg_import': True, # Flag to identify MSG import
|
|
}
|
|
|
|
try:
|
|
with self.env.cr.savepoint(): # Use savepoint for transaction management
|
|
# Process the EML using Odoo's standard message_process
|
|
thread_id = thread_model.with_context(**context).message_process(
|
|
model=self.res_model,
|
|
message=eml_content,
|
|
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.debug(
|
|
"Message processed successfully, thread_id: %s", thread_id
|
|
)
|
|
self.write(
|
|
{
|
|
"description": _("Processed as email on %s")
|
|
% fields.Datetime.now(),
|
|
"msg_processed": True,
|
|
}
|
|
)
|
|
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 eml_content if 'eml_content' in locals() else False
|
|
raise
|
|
|
|
except Exception as e:
|
|
_logger.error("Error processing MSG file %s: %s", self.name, str(e), exc_info=True)
|
|
if not self.env.context.get('skip_msg_processing_on_error'):
|
|
raise
|
|
|
|
# Get error logging preference safely
|
|
try:
|
|
with self.env.cr.savepoint():
|
|
log_errors = self.env['ir.config_parameter'].sudo().get_param(
|
|
'msg_attachments.log_msg_error', 'False'
|
|
).lower() == 'true'
|
|
except Exception:
|
|
log_errors = False # Default to False if parameter can't be read
|
|
|
|
# Si thread_id est False, on log l'erreur si configuré
|
|
if log_errors:
|
|
error_msg = f"Failed to process MSG file: {self.name} - message_process failed to create thread_id"
|
|
_msg_import_logger.error(error_msg, exc_info=True)
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
if not self.env.context.get('skip_msg_processing_on_error'):
|
|
raise
|
|
|
|
# Log l'erreur si configuré
|
|
log_errors = self.env['ir.config_parameter'].sudo().get_param(
|
|
'msg_attachments.log_msg_error', 'False'
|
|
).lower() == 'true'
|
|
|
|
if log_errors:
|
|
error_msg = f"Error processing MSG file: {self.name} - {str(e)}"
|
|
_msg_import_logger.error(error_msg, exc_info=True)
|
|
return False
|
|
|
|
def _notify_error(self, title, message):
|
|
"""
|
|
Show error notification to the user.
|
|
|
|
Args:
|
|
title (str): Title of the notification.
|
|
message (str): Message to display in the notification.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
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):
|
|
"""Override create to handle MSG files."""
|
|
if not isinstance(vals_list, list):
|
|
vals_list = [vals_list]
|
|
|
|
attachments = super().create(vals_list)
|
|
|
|
for attachment in attachments:
|
|
try:
|
|
if attachment._is_msg_file():
|
|
attachment.process_msg_as_email()
|
|
except Exception as e:
|
|
_logger.error("MSG processing failed: %s", str(e))
|
|
attachment._notify_error(_("MSG Error"), str(e))
|
|
|
|
return attachments
|
|
|
|
def write(self, vals):
|
|
"""Override write to handle problematic files on update."""
|
|
res = super().write(vals)
|
|
|
|
if 'datas' in vals:
|
|
for attachment in self:
|
|
try:
|
|
# Check if it's a PDF file
|
|
if attachment.mimetype == 'application/pdf' and attachment.store_fname:
|
|
try:
|
|
import PyPDF2
|
|
with open(attachment._full_path(attachment.store_fname), 'rb') as pdf_file:
|
|
try:
|
|
pdf_reader = PyPDF2.PdfReader(pdf_file)
|
|
num_pages = len(pdf_reader.pages)
|
|
_logger.info("[%s] PDF processed successfully, pages: %d", attachment.name or 'Unknown', num_pages)
|
|
except Exception as e:
|
|
_logger.warning(
|
|
"[%s] Failed to process PDF: %s",
|
|
attachment.name or 'Unknown', str(e)
|
|
)
|
|
attachment._move_to_review_dir('pdf_error')
|
|
except Exception as e:
|
|
_logger.error("[%s] Error accessing PDF file: %s", attachment.name or 'Unknown', str(e))
|
|
|
|
# Process MSG files
|
|
if attachment._is_msg_file():
|
|
attachment.process_msg_as_email()
|
|
|
|
except Exception as e:
|
|
_logger.error("[%s] Error processing attachment: %s", attachment.name or 'Unknown', str(e))
|
|
|
|
return res
|
|
|
|
@api.model
|
|
def process_msg_attachments_batch(self, attachment_ids, batch_size=10):
|
|
"""
|
|
Process multiple MSG attachments in batches.
|
|
|
|
Args:
|
|
attachment_ids (list): List of attachment IDs to process
|
|
batch_size (int): Number of attachments to process in each batch
|
|
|
|
Returns:
|
|
dict: Processing results
|
|
"""
|
|
results = {'success': [], 'failed': []}
|
|
attachments = self.browse(attachment_ids)
|
|
|
|
for i in range(0, len(attachments), batch_size):
|
|
batch = attachments[i:i + batch_size]
|
|
for attachment in batch:
|
|
try:
|
|
if attachment.process_msg_as_email():
|
|
results['success'].append(attachment.id)
|
|
else:
|
|
results['failed'].append(attachment.id)
|
|
except Exception as e:
|
|
_logger.error(
|
|
"Error processing attachment %s: %s",
|
|
attachment.name, str(e)
|
|
)
|
|
results['failed'].append(attachment.id)
|
|
|
|
return results
|