isalation of missing 41 to fix only
This commit is contained in:
parent
55a69d68b1
commit
549cbf2579
1 changed files with 316 additions and 329 deletions
|
|
@ -19,6 +19,10 @@ import re
|
||||||
from odoo.tools import email_normalize, email_split
|
from odoo.tools import email_normalize, email_split
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import extract_msg
|
import extract_msg
|
||||||
|
import psycopg2
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from odoo.tools import config
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
@ -104,6 +108,35 @@ class IrAttachment(models.Model):
|
||||||
str: EML file data.
|
str: EML file data.
|
||||||
"""
|
"""
|
||||||
try:
|
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
|
# Read the MSG file
|
||||||
msg_file = extract_msg.Message(BytesIO(msg_data))
|
msg_file = extract_msg.Message(BytesIO(msg_data))
|
||||||
_logger.debug(
|
_logger.debug(
|
||||||
|
|
@ -121,300 +154,148 @@ class IrAttachment(models.Model):
|
||||||
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 []
|
||||||
|
|
||||||
# Vérifier si les adresses existent dans les partenaires
|
|
||||||
partner_model = self.env["res.partner"]
|
|
||||||
from_partners = (
|
|
||||||
partner_model.search([("email", "=ilike", from_email)])
|
|
||||||
if from_email
|
|
||||||
else False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get current record's partner if exists
|
# Get current record's partner if exists
|
||||||
current_partner = False
|
current_partner = False
|
||||||
if self.res_model and self.res_id:
|
if self.res_model and self.res_id:
|
||||||
record = self.env[self.res_model].browse(self.res_id)
|
try:
|
||||||
if hasattr(record, "partner_id"):
|
record = self.env[self.res_model].browse(self.res_id)
|
||||||
current_partner = record.partner_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))
|
||||||
|
|
||||||
if not from_partners:
|
# Recherche du partenaire expéditeur avec gestion d'erreur
|
||||||
if current_partner and from_email:
|
from_partner = None
|
||||||
# Create new contact under the parent company
|
if from_email:
|
||||||
company = current_partner.commercial_partner_id
|
try:
|
||||||
# Extract name from email if possible
|
with self.env.cr.savepoint():
|
||||||
display_name = msg_file.sender
|
from_partners = self.env["res.partner"].search([
|
||||||
if "<" in display_name and ">" in display_name:
|
("email", "=ilike", from_email),
|
||||||
display_name = display_name.split("<")[0].strip()
|
("active", "in", [True, False])
|
||||||
|
])
|
||||||
from_partner = partner_model.create(
|
|
||||||
{
|
if len(from_partners) == 1:
|
||||||
"name": display_name,
|
from_partner = from_partners[0]
|
||||||
"email": from_email,
|
elif len(from_partners) > 1 and current_partner:
|
||||||
"parent_id": company.id,
|
# Search among contacts linked to the current object's company
|
||||||
"company_id": (
|
company = current_partner.commercial_partner_id
|
||||||
company.company_id.id
|
company_contacts = from_partners.filtered(
|
||||||
if company.company_id
|
lambda p: p.commercial_partner_id == company
|
||||||
else self.env.company.id
|
|
||||||
),
|
|
||||||
"type": "contact",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
_logger.info(
|
|
||||||
"Created new contact %s under company %s for email %s",
|
|
||||||
display_name,
|
|
||||||
company.name,
|
|
||||||
from_email,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
_logger.warning(
|
|
||||||
"Email sender not found in partners and no current partner to link to: %s",
|
|
||||||
from_email,
|
|
||||||
)
|
|
||||||
# Continue processing with the original sender
|
|
||||||
from_partner = None
|
|
||||||
elif len(from_partners) > 1:
|
|
||||||
if 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:
|
|
||||||
# Si on trouve des contacts liés à la compagnie, prendre le plus récent
|
|
||||||
from_partner = (
|
|
||||||
company_contacts.filtered("active").sorted(
|
|
||||||
"write_date", reverse=True
|
|
||||||
)[0]
|
|
||||||
if company_contacts.filtered("active")
|
|
||||||
else company_contacts[0]
|
|
||||||
)
|
|
||||||
_logger.info(
|
|
||||||
"Plusieurs partenaires trouvés pour l'email %s - Utilisation du contact %s lié à la compagnie %s",
|
|
||||||
from_email,
|
|
||||||
from_partner.name,
|
|
||||||
company.name,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Si aucun contact lié à la compagnie n'est trouvé, utiliser la logique précédente
|
|
||||||
company_partner = from_partners.filtered("is_company")
|
|
||||||
if len(company_partner) == 1:
|
|
||||||
from_partner = company_partner
|
|
||||||
else:
|
|
||||||
from_partner = (
|
|
||||||
from_partners.filtered("active").sorted(
|
|
||||||
"write_date", reverse=True
|
|
||||||
)[0]
|
|
||||||
if from_partners.filtered("active")
|
|
||||||
else from_partners[0]
|
|
||||||
)
|
)
|
||||||
_logger.info(
|
if company_contacts:
|
||||||
"Plusieurs partenaires trouvés pour l'email %s - Utilisation de %s (aucun contact lié à la compagnie actuelle)",
|
from_partner = company_contacts[0]
|
||||||
from_email,
|
except Exception as e:
|
||||||
from_partner.name,
|
_logger.warning("Failed to process sender partner: %s", str(e))
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Si pas de partenaire courant, utiliser la logique précédente
|
|
||||||
company_partner = from_partners.filtered("is_company")
|
|
||||||
if len(company_partner) == 1:
|
|
||||||
from_partner = company_partner
|
|
||||||
else:
|
|
||||||
from_partner = (
|
|
||||||
from_partners.filtered("active").sorted(
|
|
||||||
"write_date", reverse=True
|
|
||||||
)[0]
|
|
||||||
if from_partners.filtered("active")
|
|
||||||
else from_partners[0]
|
|
||||||
)
|
|
||||||
_logger.info(
|
|
||||||
"Plusieurs partenaires trouvés pour l'email %s - Utilisation de %s",
|
|
||||||
from_email,
|
|
||||||
from_partner.name,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
from_partner = from_partners
|
|
||||||
|
|
||||||
# Add headers
|
# Set email headers
|
||||||
email_msg["Subject"] = (
|
email_msg["Subject"] = self._clean_header_value(msg_file.subject)
|
||||||
self._clean_header_value(msg_file.subject) or "No Subject"
|
|
||||||
)
|
|
||||||
email_msg["From"] = self._clean_header_value(msg_file.sender)
|
email_msg["From"] = self._clean_header_value(msg_file.sender)
|
||||||
email_msg["To"] = self._clean_header_value(msg_file.to)
|
email_msg["To"] = self._clean_header_value(msg_file.to)
|
||||||
email_msg["Cc"] = (
|
if msg_file.cc:
|
||||||
self._clean_header_value(msg_file.cc) if msg_file.cc else None
|
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 formatdate(localtime=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add original MSG headers if present
|
# Add email body
|
||||||
for header in ["Message-ID", "In-Reply-To", "References"]:
|
|
||||||
if header.lower() in msg_file.header:
|
|
||||||
header_value = self._clean_header_value(
|
|
||||||
msg_file.header[header.lower()]
|
|
||||||
)
|
|
||||||
if header_value: # Only add header if it has a value
|
|
||||||
email_msg[header] = header_value
|
|
||||||
|
|
||||||
# Set the body
|
|
||||||
if msg_file.body:
|
if msg_file.body:
|
||||||
body = msg_file.body
|
body = html_sanitize(msg_file.body)
|
||||||
|
email_msg.attach(MIMEText(body, "html"))
|
||||||
# Check if metadata should be included
|
|
||||||
show_metadata = (
|
|
||||||
self.env["ir.config_parameter"]
|
|
||||||
.sudo()
|
|
||||||
.get_param("msg_attachments.format_metadata", "True")
|
|
||||||
.lower()
|
|
||||||
== "true"
|
|
||||||
)
|
|
||||||
|
|
||||||
if show_metadata:
|
|
||||||
# Format the metadata section
|
|
||||||
metadata_html = f"""
|
|
||||||
<div style="background-color: #f8f9fa; padding: 10px; margin-bottom: 15px; border: 1px solid #dee2e6; border-radius: 4px; font-family: Arial, sans-serif; font-size: 13px;">
|
|
||||||
<div style="font-weight: bold; color: #495057; margin-bottom: 8px;">Original Email Details:</div>
|
|
||||||
<table style="border-collapse: collapse; width: 100%;">
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 2px 8px 2px 0; color: #495057; width: 100px;"><strong>From:</strong></td>
|
|
||||||
<td style="padding: 2px 0;">{html_sanitize(msg_file.sender or "")}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 2px 8px 2px 0; color: #495057;"><strong>Date:</strong></td>
|
|
||||||
<td style="padding: 2px 0;">{msg_file.date.strftime("%Y-%m-%d %H:%M:%S") if msg_file.date else "Unknown"}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 2px 8px 2px 0; color: #495057;"><strong>To:</strong></td>
|
|
||||||
<td style="padding: 2px 0;">{html_sanitize(msg_file.to or "")}</td>
|
|
||||||
</tr>
|
|
||||||
"""
|
|
||||||
|
|
||||||
if msg_file.cc:
|
|
||||||
metadata_html += f"""
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 2px 8px 2px 0; color: #495057;"><strong>CC:</strong></td>
|
|
||||||
<td style="padding: 2px 0;">{html_sanitize(msg_file.cc)}</td>
|
|
||||||
</tr>
|
|
||||||
"""
|
|
||||||
|
|
||||||
if "message-id" in msg_file.header:
|
|
||||||
metadata_html += f"""
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 2px 8px 2px 0; color: #495057;"><strong>Message-ID:</strong></td>
|
|
||||||
<td style="padding: 2px 0; word-break: break-all;">{html_sanitize(msg_file.header["message-id"])}</td>
|
|
||||||
</tr>
|
|
||||||
"""
|
|
||||||
|
|
||||||
metadata_html += """
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Check if the body is HTML or contains HTML-like content
|
|
||||||
is_html = "<html" in body.lower() or any(
|
|
||||||
tag in body.lower() for tag in ["<div", "<p", "<br", "<table", "<a"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not is_html:
|
|
||||||
# Convert plain text to HTML
|
|
||||||
body = body.replace("\n", "<br>")
|
|
||||||
# Convert URLs to links
|
|
||||||
body = re.sub(
|
|
||||||
r'(https?://[^\s<>"]+|www\.[^\s<>"]+)',
|
|
||||||
r'<a href="\1">\1</a>',
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
# Convert CID references to img tags
|
|
||||||
if "[cid:" in body:
|
|
||||||
body = re.sub(
|
|
||||||
r"\[cid:([^\]]+)\]",
|
|
||||||
lambda m: f'<img src="cid:{m.group(1)}" alt="{m.group(1)}">',
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
# Wrap in HTML tags
|
|
||||||
body = f'<div style="font-family: Arial, sans-serif; font-size: 13px;">{body}</div>'
|
|
||||||
|
|
||||||
if show_metadata:
|
|
||||||
# Add metadata at the top
|
|
||||||
body = metadata_html + body
|
|
||||||
|
|
||||||
# Always sanitize HTML
|
|
||||||
body = html_sanitize(
|
|
||||||
body, sanitize_tags=False, sanitize_attributes=False, sanitize_style=False
|
|
||||||
)
|
|
||||||
|
|
||||||
html_part = MIMEText(body, "html")
|
|
||||||
email_msg.attach(html_part)
|
|
||||||
else:
|
|
||||||
html_part = MIMEText("", "html")
|
|
||||||
email_msg.attach(html_part)
|
|
||||||
|
|
||||||
# Add attachments
|
|
||||||
for attachment in msg_file.attachments:
|
|
||||||
if not hasattr(attachment, "data") or not attachment.data:
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
# Process attachments
|
||||||
|
for att in msg_file.attachments:
|
||||||
try:
|
try:
|
||||||
filename = attachment.getFilename()
|
filename = att.longFilename or att.shortFilename
|
||||||
except AttributeError:
|
if filename:
|
||||||
filename = "unknown.bin"
|
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)
|
||||||
|
except Exception as e:
|
||||||
_logger.warning(
|
_logger.warning(
|
||||||
"Could not get filename for attachment, using default: %s",
|
"Failed to process attachment %s: %s - Attachment data type: %s, Content: %s",
|
||||||
filename,
|
filename,
|
||||||
|
str(e),
|
||||||
|
type(att.data),
|
||||||
|
str(att.data)[:100] if att.data else "None"
|
||||||
)
|
)
|
||||||
|
# Move problematic attachment to review directory
|
||||||
if hasattr(attachment, "contentId") and attachment.contentId:
|
self._move_to_review_dir('msg_conversion')
|
||||||
# Image avec CID - utiliser MIMEImage
|
|
||||||
maintype, subtype = (attachment.mimetype or "image/jpeg").split(
|
|
||||||
"/", 1
|
|
||||||
)
|
|
||||||
if maintype == "image":
|
|
||||||
mime_part = MIMEImage(attachment.data, _subtype=subtype)
|
|
||||||
mime_part.add_header(
|
|
||||||
"Content-ID", f"<{attachment.contentId.strip('<>')}>"
|
|
||||||
)
|
|
||||||
mime_part.add_header(
|
|
||||||
"Content-Disposition", "inline", filename=filename
|
|
||||||
)
|
|
||||||
_logger.debug(
|
|
||||||
"Adding inline image with CID: %s, filename: %s",
|
|
||||||
attachment.contentId,
|
|
||||||
filename,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Fallback pour les non-images avec CID
|
|
||||||
mime_part = MIMEBase(maintype, subtype)
|
|
||||||
mime_part.set_payload(attachment.data)
|
|
||||||
encoders.encode_base64(mime_part)
|
|
||||||
mime_part.add_header(
|
|
||||||
"Content-ID", f"<{attachment.contentId.strip('<>')}>"
|
|
||||||
)
|
|
||||||
mime_part.add_header(
|
|
||||||
"Content-Disposition", "inline", filename=filename
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Pièce jointe normale
|
|
||||||
maintype, subtype = (
|
|
||||||
attachment.mimetype or "application/octet-stream"
|
|
||||||
).split("/", 1)
|
|
||||||
mime_part = MIMEBase(maintype, subtype)
|
|
||||||
mime_part.set_payload(attachment.data)
|
|
||||||
encoders.encode_base64(mime_part)
|
|
||||||
mime_part.add_header(
|
|
||||||
"Content-Disposition", "attachment", filename=filename
|
|
||||||
)
|
|
||||||
|
|
||||||
email_msg.attach(mime_part)
|
|
||||||
|
|
||||||
return email_msg.as_string()
|
return email_msg.as_string()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_msg_import_logger.error(
|
_logger.error("Error converting MSG to EML: %s", str(e))
|
||||||
"Error converting MSG to EML: %s", str(e), exc_info=True
|
|
||||||
)
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
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):
|
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.
|
||||||
|
|
@ -422,31 +303,40 @@ class IrAttachment(models.Model):
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if successful, False otherwise
|
bool: True if successful, False otherwise
|
||||||
"""
|
"""
|
||||||
self.ensure_one()
|
|
||||||
_logger.debug(
|
|
||||||
"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():
|
|
||||||
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"Invalid MSG file: {self.name} (model: {self.res_model}, res_id: {self.res_id})"
|
|
||||||
if log_errors and log_file:
|
|
||||||
_msg_import_logger.error(error_msg)
|
|
||||||
self._notify_error(
|
|
||||||
"Invalid File", _("The selected file is not a valid MSG file")
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Convert MSG to EML
|
self.ensure_one()
|
||||||
msg_data = base64.b64decode(self.datas)
|
|
||||||
eml_content = self._msg_to_eml(msg_data)
|
|
||||||
|
|
||||||
# Process using Odoo's standard mail handling
|
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
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Convert base64 to bytes with validation
|
||||||
|
msg_data = base64.b64decode(self.datas)
|
||||||
|
if not isinstance(msg_data, bytes):
|
||||||
|
raise ValueError(f"Expected bytes after b64decode, got {type(msg_data)}")
|
||||||
|
|
||||||
|
eml_content = self._msg_to_eml(msg_data)
|
||||||
|
if not isinstance(eml_content, str):
|
||||||
|
raise ValueError(f"Expected string EML content, got {type(eml_content)}")
|
||||||
|
|
||||||
|
except (ValueError, TypeError) 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
|
||||||
|
|
||||||
|
# Get thread model based on res_model
|
||||||
thread_model = (
|
thread_model = (
|
||||||
self.env[self.res_model] if self.res_model else self.env["mail.thread"]
|
self.env[self.res_model] if self.res_model else self.env["mail.thread"]
|
||||||
)
|
)
|
||||||
|
|
@ -461,45 +351,78 @@ class IrAttachment(models.Model):
|
||||||
"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,
|
||||||
|
"mail_create_skip_followers": True,
|
||||||
|
"mail_auto_subscribe_no_notify": True, # Prevent auto-subscription notifications
|
||||||
|
"no_auto_thread": True # Prevent automatic thread creation
|
||||||
}
|
}
|
||||||
|
|
||||||
# Process the EML using Odoo's standard message_process
|
try:
|
||||||
thread_id = thread_model.with_context(**context).message_process(
|
with self.env.cr.savepoint(): # Use savepoint for transaction management
|
||||||
model=self.res_model,
|
# Process the EML using Odoo's standard message_process
|
||||||
message=eml_content,
|
thread_id = thread_model.with_context(**context).message_process(
|
||||||
custom_values={
|
model=self.res_model,
|
||||||
"res_id": self.res_id,
|
message=eml_content,
|
||||||
"model": self.res_model,
|
custom_values={
|
||||||
"msg_attachment_id": self.id,
|
"res_id": self.res_id,
|
||||||
},
|
"model": self.res_model,
|
||||||
thread_id=self.res_id,
|
"msg_attachment_id": self.id,
|
||||||
)
|
},
|
||||||
|
thread_id=self.res_id,
|
||||||
|
)
|
||||||
|
|
||||||
if thread_id:
|
if thread_id:
|
||||||
_logger.debug(
|
_logger.debug(
|
||||||
"Message processed successfully, thread_id: %s", thread_id
|
"Message processed successfully, thread_id: %s", thread_id
|
||||||
)
|
)
|
||||||
self.write(
|
self.write(
|
||||||
{
|
{
|
||||||
"description": _("Processed as email on %s")
|
"description": _("Processed as email on %s")
|
||||||
% fields.Datetime.now(),
|
% fields.Datetime.now(),
|
||||||
"msg_processed": True,
|
"msg_processed": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return thread_id
|
return thread_id
|
||||||
else:
|
|
||||||
log_errors = self.env['ir.config_parameter'].sudo().get_param('msg_attachments.log_msg_error', 'False').lower() == 'true'
|
except psycopg2.IntegrityError as e:
|
||||||
log_file = self.env['ir.config_parameter'].sudo().get_param('msg_attachments.log_file_path')
|
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
|
||||||
|
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"
|
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)
|
_msg_import_logger.error(error_msg, exc_info=True)
|
||||||
return False
|
|
||||||
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_errors = self.env['ir.config_parameter'].sudo().get_param('msg_attachments.log_msg_error', 'False').lower() == 'true'
|
if not self.env.context.get('skip_msg_processing_on_error'):
|
||||||
log_file = self.env['ir.config_parameter'].sudo().get_param('msg_attachments.log_file_path')
|
raise
|
||||||
error_msg = f"Error processing MSG file: {self.name} - {str(e)}"
|
|
||||||
if log_errors and log_file:
|
# 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)
|
_msg_import_logger.error(error_msg, exc_info=True)
|
||||||
|
|
||||||
self._notify_error(
|
self._notify_error(
|
||||||
"Processing Error", _("Could not process the MSG file: %s") % str(e)
|
"Processing Error", _("Could not process the MSG file: %s") % str(e)
|
||||||
)
|
)
|
||||||
|
|
@ -529,8 +452,72 @@ class IrAttachment(models.Model):
|
||||||
|
|
||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
def create(self, vals_list):
|
def create(self, vals_list):
|
||||||
|
"""Override create to handle problematic files."""
|
||||||
|
if not isinstance(vals_list, list):
|
||||||
|
vals_list = [vals_list]
|
||||||
|
|
||||||
attachments = super().create(vals_list)
|
attachments = super().create(vals_list)
|
||||||
|
|
||||||
for attachment in attachments:
|
for attachment in attachments:
|
||||||
if attachment._is_msg_file():
|
try:
|
||||||
attachment.process_msg_as_email()
|
# Check if it's a PDF file
|
||||||
|
if attachment.mimetype == 'application/pdf' and attachment.store_fname:
|
||||||
|
# Try to open and read the PDF
|
||||||
|
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 attachments
|
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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue