diff --git a/TEST.eml b/TEST.eml deleted file mode 100644 index e0bffa2..0000000 --- a/TEST.eml +++ /dev/null @@ -1,1317 +0,0 @@ -From: Miguel Angel Vera - Vetromeccanica S.r.l. -To: Miguel Angel Vera - Vetromeccanica S.r.l. -Subject: TEST Tables -Thread-Topic: TEST Tables -Thread-Index: AQHbd7PepN5tf2wDskaE081T3aMPKQ== -Date: Wed, 5 Feb 2025 09:53:55 +0000 -Message-ID: - -Content-Language: es-ES -X-MS-Has-Attach: -X-MS-TNEF-Correlator: -X-MS-Exchange-Organization-RecordReviewCfmType: 0 -msip_labels: -Content-Type: multipart/alternative; - boundary="_000_AS8PR08MB682139DC577AE00699D695149CF72AS8PR08MB6821eurp_" -MIME-Version: 1.0 - ---_000_AS8PR08MB682139DC577AE00699D695149CF72AS8PR08MB6821eurp_ -Content-Type: text/plain; charset="Windows-1252" -Content-Transfer-Encoding: quoted-printable - -Allego le email che riassumono il funzionamento del batch handling. Riporto= - sotto la parte principale. - - - -We confirm the following code sas per attached e-mail) are matching with wh= -at we agreed back in May therefore we can process P/N (customer article) b= -ut ALPLA should write in our supervision PLC a list where customer article = -is associated to AV, AV Desc, Product Family as agreed. - -This list will be constantly editable by ALPLA on a persistent memory as ag= -reed in order for Vetromeccanica to handle new customer articles except for= - new formats (new bottle shapes) which will require new recipes and commis= -sioning first. - -As per now we only have an excel list dated May 2020 which we cannot use fo= -r batch handling. - -ALPLA AV - -Blank Bottle P/N - -184 - -2638879 - -253 - -2688129 - -102 - -2638876 - - - - - - - - - - - -Agreed: - - * AV (must be forwared to Autefa, must be shown on your HMI=92s) - * AV Desc (must be shown on you HMI=92s) - * Product Family (must be used by Vetro to select right Recipe) - * Customer article Number (used by Vetro to find matching AV) - - - - - -HENKEL - -Alpla - -Vetromeccanica - -AUTEFA - -Before Changeover - -Sends IDH_BTL_NEXT number to be validated - - - - - - - -Data_To_EbConvey[38] - - - - - - - - - -Validates IDH_BTL_NEXT is valid number and send acknowledge - - - - - - - -Data_From_EbConvey[0].2 - - - - - -Step 1 - -Operator manually selects "Line Clearance" (?) and send signal "0" on "Calc= -ulatedBottlesRemainingToFill". - - - - - - - - - -Data_To_EbConvey[23] - - - - - - - -Step 2 - - - -Stops taking bottles out of trays and sends what is already on the tables a= -nd conveyors. - -Emptying Merger and Line - -Stops taking bottles out of trays and sends what is already on the tables a= -nd conveyors. - - - - - -N/A - -TG10 Send 0 in Bottles for Actual Batch - - - -Step 3 - -Operator verifies line is empty and sends "changeover request" signal after= - last bottle goes thorugh filler. - - - - - - - - - -Data_To_EbConvey[0].0 - - - - - - - -Step 4 - - - -Operator verifies line is empty and sends "line is busy with changeover", c= -onfirming on Popup screen - - - - - - - - - -Data_From_EbConvey[0].0 - - - - - -Step 5 - - - -Starts changeover - -Starts changeover - - - - - - - -Data_From_EbConvey[0].0 - -Data_From_EbConvey[0].0 - -Step 6 - - - - - -Ends changeover. Sends signal "Changeover is finished and ready. - -Ends changeover. Sends signal "Changeover is finished and ready. - - - - - - - -Data_From_EbConvey[0].1 - -Data_From_EbConvey[0].1 - - - - - - - - - - - - - - - - - -Step 7 - -Send Reset counters signal - -Reset Counters - - - - - -Data_To_EbConvey[0].1 - - - - - - - -Step 8 - -Send new value on "CalculatedBottlesRemainingToFill". - - - - - - - -Data_To_EbConvey[23] - - - - - - - -Step 9 - -Send Changeover Complete. To be considered "Production Ready" - -Starts conveying bottles. - -Finish Chanover Cycle - - - - - -Data_To_EbConvey[0].2 - - - - - - - - - - ---_000_AS8PR08MB682139DC577AE00699D695149CF72AS8PR08MB6821eurp_ -Content-Type: text/html; charset="Windows-1252" -Content-Transfer-Encoding: quoted-printable - - - - - - - -

-Allego le email che riassumono il funzionamento del batch h= -andling. Riporto sotto la parte principale.

-

- 

-

-We confirm the following code sas per attached e-mail) a= -re matching with what we agreed back in May therefore we can process P/N (c= -ustomer article)  but ALPLA should - write in our supervision PLC a list where customer article is associated t= -o  AV, AV Desc, Product Family as agreed.

-

-This list will be constantly editable by ALPLA on a pers= -istent memory as agreed in order for Vetromeccanica to handle new customer = -articles except for new formats (new - bottle shapes) which will  require new recipes and commissioning firs= -t.

-

-As per now we only have an excel list dated May 2020 whi= -ch we cannot use for batch handling.

- - - - - - - - - - - - - - - - - - - -
-

-ALPLA AV

-
-

-Blank Bottle P/N

-
-

-184

-
-

-2638879

-
-

-253

-
-

-2688129

-
-

-102

-
-

-2638876

-
-

- 

-

- 

-

- 

-

- 

-

- 

-

-Agreed:

-
    -
  • -AV  (must be forwared to Autefa, must be shown on you= -r HMI=92s)
  • -AV Desc  (must be shown on you HMI=92s)
  • -Product Family (must be used by Vetro to select right Reci= -pe)
  • -Customer article Number (used by Vetro to find matching AV= -)
-

- 

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

- 

-
-

-HENKEL

-
-

-Alpla

-
-

-Vetromeccanica

-
-

-AUTEFA

-
-

-Before Changeover

-
-

-Sends IDH_BTL_NEXT number to be validated

-
-

- 

-
-

- 

-
-

- 

-
-

-Data_To_EbConvey[38]= -

-
-

- 

-
-

- 

-
-

- 

-
-

- 

-
-

-Validates IDH_BTL_NEXT is valid number and se= -nd acknowledge

-
-

- 

-
-

- 

-
-

- 

-
-

-Data_From_EbConvey[0].2

-
-

- 

-
-

- 

-
-

-Step 1

-
-

-Operator manually selects "Line Clearanc= -e" (?) and send signal "0" on "CalculatedBottlesRemaini= -ngToFill".

-
-

- 

-
-

- 

-
-

- 

-
-

- 

-
-

-Data_To_EbConvey[23]= -

-
-

- 

-
-

- 

-
-

- 

-
-

-Step 2

-
-

- 

-
-

-Stops taking bottles out of trays and sends w= -hat is already on the tables and conveyors.

-
-

-Emptying Merger and Line

-
-

-Stops taking bottles out of trays and sends w= -hat is already on the tables and conveyors.

-
-

- 

-
-

- 

-
-

-N/A

-
-

-TG10 Send 0 in Bottles for Actu= -al Batch

-
-

- 

-
-

-Step 3

-
-

-Operator verifies line is empty and sends &qu= -ot;changeover request" signal after last bottle goes thorugh filler.

-
-

- 

-
-

- 

-
-

- 

-
-

- 

-
-

-Data_To_EbConvey[0].0

-
-

- 

-
-

- 

-
-

- 

-
-

-Step 4

-
-

- 

-
-

-Operator verifies line is empty and sends &qu= -ot;line is busy with changeover", confirming on Popup screen -

-

- 

-
-

- 

-
-

- 

-
-

- 

-
-

-Data_From_EbConvey[0].0

-
-

- 

-
-

- 

-
-

-Step 5

-
-

- 

-
- -

-Starts changeover

-
-

-Starts changeover

-
-

- 

-
-

- 

-
-

- 

-
-

-Data_From_EbConvey[0].0

-
-

-Data_From_EbConvey[0].0

-
-

-Step 6

-
-

- 

-
-

- 

-
-

-Ends changeover. Sends signal "Changeove= -r is finished and ready.

-
-

-Ends changeover. Sends signal "Changeove= -r is finished and ready.

-
-

- 

-
-

- 

-
-

- 

-
-

-Data_From_EbConvey[0].1

-
-

-Data_From_EbConvey[0].1

-
-

- 

-
-

- 

-
- -

- 

-
-

- 

-
-

- 

-
-

- 

-
- -

- 

-
-

- 

-
-

-Step 7

-
-

-Send Reset counters signal

-
- -

-Reset Counters

-
-

- 

-
-

- 

-
-

-Data_To_EbConvey[0].1

-
-

- 

-
-

- 

-
-

- 

-
-

-Step 8

-
-

-Send new value on "CalculatedBottlesRema= -iningToFill".

-
- -

- 

-
-

- 

-
-

- 

-
-

-Data_To_EbConvey[23]= -

-
-

- 

-
-

- 

-
-

- 

-
-

-Step 9

-
-

-Send Changeover Complete. To be considered &q= -uot;Production Ready" 

-
-

-Starts conveying bottles.

-
-

-Finish Chanover Cycle

-
-

- 

-
-

- 

-
-

-Data_To_EbConvey[0].2

-
-

- 

-
-

- 

-
-

- 

-
-

- 

-
-
-
- - - ---_000_AS8PR08MB682139DC577AE00699D695149CF72AS8PR08MB6821eurp_-- diff --git a/utils/__pycache__/email_parser.cpython-310.pyc b/utils/__pycache__/email_parser.cpython-310.pyc index 15fe621..f6974d3 100644 Binary files a/utils/__pycache__/email_parser.cpython-310.pyc and b/utils/__pycache__/email_parser.cpython-310.pyc differ diff --git a/utils/__pycache__/forward_handler.cpython-310.pyc b/utils/__pycache__/forward_handler.cpython-310.pyc new file mode 100644 index 0000000..3892e80 Binary files /dev/null and b/utils/__pycache__/forward_handler.cpython-310.pyc differ diff --git a/utils/email_parser.py b/utils/email_parser.py index 6ba70e9..833c686 100644 --- a/utils/email_parser.py +++ b/utils/email_parser.py @@ -9,6 +9,7 @@ from bs4 import BeautifulSoup from email.utils import parsedate_to_datetime from models.mensaje_email import MensajeEmail from utils.attachment_handler import guardar_adjunto +from utils.forward_handler import extract_forwarded_messages import tempfile import os @@ -183,96 +184,6 @@ def procesar_eml(ruta_archivo, dir_adjuntos): print(f"Error al abrir el archivo {ruta_archivo}: {str(e)}") return [] -def procesar_eml_interno(mensaje, dir_adjuntos): - """ - Procesa un mensaje de email, ya sea desde archivo o adjunto - """ - mensajes = [] - - try: - remitente = mensaje.get('from', '') - fecha_str = mensaje.get('date', '') - fecha = _parsear_fecha(fecha_str) - - # Get subject from email headers first - subject = mensaje.get('subject', '') - if subject: - # Try to decode if it's encoded - subject = str(email.header.make_header(email.header.decode_header(subject))) - - contenido = "" - adjuntos = [] - tiene_html = False - - # First pass: check for HTML content - if mensaje.is_multipart(): - for parte in mensaje.walk(): - if parte.get_content_type() == "text/html": - tiene_html = True - break - else: - tiene_html = mensaje.get_content_type() == "text/html" - - # Second pass: process content and attachments - if mensaje.is_multipart(): - for parte in mensaje.walk(): - content_type = parte.get_content_type() - - try: - if content_type == "text/html": - html_content = _get_payload_safely(parte) - if html_content: - part_subject, text = _html_a_markdown(html_content) - if not subject and part_subject: - subject = part_subject - if text: - contenido = text - elif content_type == "text/plain" and not tiene_html: - text = _get_payload_safely(parte) - if text: - contenido = text - elif content_type == "message/rfc822": - # Procesar email adjunto - mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos) - mensajes.extend(mensajes_adjuntos) - elif parte.get_content_disposition() == 'attachment': - nombre = parte.get_filename() - if nombre and nombre.lower().endswith('.eml'): - # Si es un archivo .eml adjunto - mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos) - mensajes.extend(mensajes_adjuntos) - else: - # Otros tipos de adjuntos - ruta_adjunto = guardar_adjunto(parte, dir_adjuntos) - if ruta_adjunto: - adjuntos.append(Path(ruta_adjunto).name) - except Exception as e: - print(f"Error procesando parte del mensaje: {str(e)}") - continue - else: - if mensaje.get_content_type() == "text/html": - html_content = _get_payload_safely(mensaje) - if html_content: - part_subject, contenido = _html_a_markdown(html_content) - if not subject and part_subject: - subject = part_subject - else: - contenido = _get_payload_safely(mensaje) or "" - - # Solo agregar el mensaje si tiene contenido útil - if contenido or subject or adjuntos: - mensajes.append(MensajeEmail( - remitente=remitente, - fecha=fecha, - contenido=contenido, - subject=subject, - adjuntos=adjuntos - )) - - except Exception as e: - print(f"Error procesando mensaje: {str(e)}") - - return mensajes def _parsear_fecha(fecha_str): try: @@ -292,4 +203,88 @@ def _parsear_fecha(fecha_str): return datetime(int(año), mes_num, int(dia), int(hora), int(minuto)) except: pass - return datetime.now() \ No newline at end of file + return datetime.now() + +def procesar_eml_interno(mensaje, dir_adjuntos): + """ + Procesa un mensaje de email, ya sea desde archivo o adjunto + """ + mensajes = [] + + try: + remitente = mensaje.get('from', '') + fecha_str = mensaje.get('date', '') + fecha = _parsear_fecha(fecha_str) + + # Get subject from email headers first + subject = mensaje.get('subject', '') + if subject: + subject = str(email.header.make_header(email.header.decode_header(subject))) + + contenido = "" + adjuntos = [] + contenido_html = None + + # First pass: check for HTML content and extract it + if mensaje.is_multipart(): + for parte in mensaje.walk(): + content_type = parte.get_content_type() + + try: + if content_type == "text/html": + html_content = _get_payload_safely(parte) + if html_content: + contenido_html = html_content + elif content_type == "text/plain" and not contenido_html: + contenido = _get_payload_safely(parte) or "" + elif content_type == "message/rfc822": + mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos) + mensajes.extend(mensajes_adjuntos) + elif parte.get_content_disposition() == 'attachment': + nombre = parte.get_filename() + if nombre and nombre.lower().endswith('.eml'): + mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos) + mensajes.extend(mensajes_adjuntos) + else: + ruta_adjunto = guardar_adjunto(parte, dir_adjuntos) + if ruta_adjunto: + adjuntos.append(Path(ruta_adjunto).name) + except Exception as e: + print(f"Error procesando parte del mensaje: {str(e)}") + continue + else: + if mensaje.get_content_type() == "text/html": + contenido_html = _get_payload_safely(mensaje) + else: + contenido = _get_payload_safely(mensaje) or "" + + # Process HTML content if available + if contenido_html: + part_subject, text = _html_a_markdown(contenido_html) + if not subject and part_subject: + subject = part_subject + contenido = text + + # Process forwarded messages from the markdown content + if contenido: + print(f"\nBuscando mensajes reenviados en contenido ({len(contenido)} chars)") + print("Primeros 200 chars:", contenido[:200]) + contenido_principal, mensajes_reenviados = extract_forwarded_messages(contenido) + print(f"Encontrados {len(mensajes_reenviados)} mensajes reenviados") + contenido = contenido_principal + mensajes.extend(mensajes_reenviados) + + # Solo agregar el mensaje principal si tiene contenido útil + if contenido or subject or adjuntos: + mensajes.insert(0, MensajeEmail( + remitente=remitente, + fecha=fecha, + contenido=contenido, + subject=subject, + adjuntos=adjuntos + )) + + except Exception as e: + print(f"Error procesando mensaje: {str(e)}") + + return mensajes \ No newline at end of file diff --git a/utils/forward_handler.py b/utils/forward_handler.py new file mode 100644 index 0000000..d17b407 --- /dev/null +++ b/utils/forward_handler.py @@ -0,0 +1,144 @@ +# utils/forward_handler.py +import re +import os +from datetime import datetime +from email.utils import parseaddr +from models.mensaje_email import MensajeEmail + +# Patrones de inicio de mensaje reenviado en diferentes idiomas +FORWARD_PATTERNS = [ + r"[-]{3,}\s*Messaggio originale\s*[-]{3,}", # Italiano + r"[-]{3,}\s*Original Message\s*[-]{3,}", # Inglés + r"[-]{3,}\s*Mensaje original\s*[-]{3,}", # Español + r"[-]{3,}\s*Message d'origine\s*[-]{3,}", # Francés + r"[-]{3,}\s*Ursprüngliche Nachricht\s*[-]{3,}", # Alemán + # Variantes más flexibles + r"[-]{3,}\s*Forwarded message\s*[-]{3,}", + r"[-]{3,}\s*Mensaje reenviado\s*[-]{3,}", + r"[-]{3,}\s*Messaggio inoltrato\s*[-]{3,}", + # Patrones con > que suelen aparecer en texto plano + r"(?m)^>\s*[-]{3,}\s*Messaggio originale\s*[-]{3,}", + r"(?m)^>\s*[-]{3,}\s*Original Message\s*[-]{3,}" +] + +# Patrones de headers en diferentes idiomas +HEADER_PATTERNS = { + 'from': [ + r"Da:\s*(.*)", # Italiano + r"From:\s*(.*)", # Inglés + r"De:\s*(.*)", # Español + r"Von:\s*(.*)", # Alemán + r"De :\s*(.*)" # Francés + ], + 'date': [ + r"Inviato:\s*(.*)", # Italiano + r"Sent:\s*(.*)", # Inglés + r"Enviado:\s*(.*)", # Español + r"Gesendet:\s*(.*)", # Alemán + r"Envoyé :\s*(.*)" # Francés + ], + 'subject': [ + r"Oggetto:\s*(.*)", # Italiano + r"Subject:\s*(.*)", # Inglés + r"Asunto:\s*(.*)", # Español + r"Betreff:\s*(.*)", # Alemán + r"Sujet :\s*(.*)" # Francés + ] +} + +def extract_forwarded_messages(contenido): + """ + Extrae mensajes reenviados del contenido del email + Retorna una lista de objetos MensajeEmail + """ + mensajes = [] + + # Crear el patrón de división combinando todos los patrones de reenvío + split_pattern = '|'.join(f"({pattern})" for pattern in FORWARD_PATTERNS) + + # Dividir el contenido usando el patrón combinado + partes = re.split(split_pattern, contenido) + + # El primer elemento es el contenido original del email + contenido_original = partes[0].strip() + + # Procesar cada parte que coincide con un patrón de reenvío + for i in range(1, len(partes), len(FORWARD_PATTERNS) + 1): + # Encontrar qué patrón coincidió + patron_encontrado = next((p for p in partes[i:i+len(FORWARD_PATTERNS)] if p), None) + if patron_encontrado and i + len(FORWARD_PATTERNS) < len(partes): + contenido_reenviado = partes[i + len(FORWARD_PATTERNS)].strip() + if contenido_reenviado: + mensaje = _procesar_contenido_reenviado(contenido_reenviado) + if mensaje: + mensajes.append(mensaje) + + return contenido_original, mensajes + +def _procesar_contenido_reenviado(contenido): + """ + Procesa el contenido de un mensaje reenviado y extrae la información relevante + """ + # Extraer headers + remitente = None + fecha_str = None + subject = None + cuerpo = contenido + + # Buscar headers al inicio del mensaje + lineas = contenido.split('\n') + headers_encontrados = 0 + i = 0 + + while i < len(lineas) and headers_encontrados < 3: + linea = lineas[i].strip() + + # Buscar remitente + if not remitente: + for pattern in HEADER_PATTERNS['from']: + match = re.match(pattern, linea) + if match: + remitente = match.group(1).strip() + headers_encontrados += 1 + break + + # Buscar fecha + if not fecha_str: + for pattern in HEADER_PATTERNS['date']: + match = re.match(pattern, linea) + if match: + fecha_str = match.group(1).strip() + headers_encontrados += 1 + break + + # Buscar asunto + if not subject: + for pattern in HEADER_PATTERNS['subject']: + match = re.match(pattern, linea) + if match: + subject = match.group(1).strip() + headers_encontrados += 1 + break + + i += 1 + + # Si encontramos headers, el cuerpo comienza después de ellos + if headers_encontrados > 0: + cuerpo = '\n'.join(lineas[i:]).strip() + + # Si no tenemos la información mínima necesaria, retornar None + if not (remitente or fecha_str or cuerpo): + return None + + # Crear el objeto MensajeEmail + try: + return MensajeEmail( + remitente=remitente or "Remitente Desconocido", + fecha=fecha_str or datetime.now(), + contenido=cuerpo, + subject=subject, + adjuntos=[] + ) + except Exception as e: + print(f"Error creando mensaje reenviado: {str(e)}") + return None \ No newline at end of file