Se añadió la creación del directorio `adjuntos/cronologia` en `x1.py` para almacenar imágenes de correos electrónicos. Se actualizó la función `procesar_eml` para manejar imágenes inline y adjuntas, y se refactorizó el código en `email_parser.py` para incluir la lógica de incrustación de imágenes en Markdown. Además, se mejoró la documentación en `MemoriaDeEvolucion.md` para reflejar estos cambios y se optimizó el manejo de errores en varias funciones.

This commit is contained in:
Miguel 2025-08-08 15:07:32 +02:00
parent 59cb4f4063
commit fc85347a43
8 changed files with 451 additions and 180 deletions

View File

@ -39,3 +39,20 @@ Salida Markdown: Escribe el índice seguido del contenido formateado en Markdown
- Impacto: - Impacto:
- Los `work_dir.json` existentes deben actualizar la clave a `input_directory`. - Los `work_dir.json` existentes deben actualizar la clave a `input_directory`.
- No hay cambios en claves de `level2` (`cronologia_file`, `attachments_dir`). - No hay cambios en claves de `level2` (`cronologia_file`, `attachments_dir`).
## 2025-08-08 — Manejo de imágenes (inline y adjuntas) y embebido en Markdown
- Decisión:
- Capturar imágenes tanto adjuntas (`attachment`) como inline (`inline`/sin `Content-Disposition`).
- Guardar las imágenes en el directorio de adjuntos configurado y además copiar a `adjuntos/cronologia` dentro del `working_directory`.
- Incrustar en el Markdown enlaces de Obsidian con ruta absoluta al archivo copiado en `adjuntos/cronologia` usando la sintaxis de embed `![[...]]` bajo una sección `### Imágenes` por mensaje.
- Cambios:
- `utils/attachment_handler.py`: nueva función `guardar_imagen` que genera nombres a partir de `Content-ID` o hash y evita colisiones por contenido; refactor de hashing de contenido.
- `utils/email_parser.py`:
- Se amplía la firma de `procesar_eml`/`procesar_eml_interno` para recibir `dir_adjuntos_cronologia` y copiar allí las imágenes.
- Se manejan imágenes en partes `attachment` y `inline`, agregando su ruta absoluta copiada a `mensaje.imagenes_cronologia`.
- `models/mensaje_email.py`: `to_markdown()` agrega sección `### Imágenes` con `![[ruta_absoluta]]` previo a `### Adjuntos`.
- `x1.py`: crea `adjuntos/cronologia` y pasa la ruta al parser.
- Impacto:
- El `.md` resultante muestra las imágenes embebidas (Obsidian) desde rutas absolutas bajo `.../adjuntos/cronologia/...`.
- Se preserva el listado de adjuntos como enlaces `[[archivo]]`.

View File

@ -6,7 +6,14 @@ from email.utils import parseaddr, parsedate_to_datetime
class MensajeEmail: class MensajeEmail:
def __init__(self, remitente, fecha, contenido, subject=None, adjuntos=None): def __init__(
self,
remitente,
fecha,
contenido,
subject=None,
adjuntos=None,
):
self.remitente = self._estandarizar_remitente(remitente) self.remitente = self._estandarizar_remitente(remitente)
self.fecha = self._estandarizar_fecha(fecha) self.fecha = self._estandarizar_fecha(fecha)
self.subject = subject if subject else "Sin Asunto" self.subject = subject if subject else "Sin Asunto"
@ -37,17 +44,26 @@ class MensajeEmail:
for line in lines: for line in lines:
# Skip metadata lines # Skip metadata lines
if line.strip().startswith( if line.strip().startswith(
("Da: ", "Inviato: ", "A: ", "From: ", "Sent: ", "To: ") (
"Da: ",
"Inviato: ",
"A: ",
"From: ",
"Sent: ",
"To: ",
)
) or line.strip().startswith("Oggetto: "): ) or line.strip().startswith("Oggetto: "):
continue continue
# Limpiar espacios múltiples dentro de cada línea, pero mantener la línea completa # Limpiar espacios múltiples dentro de cada línea, manteniendo
# la línea completa
cleaned_line = re.sub(r" +", " ", line) cleaned_line = re.sub(r" +", " ", line)
cleaned_lines.append(cleaned_line) cleaned_lines.append(cleaned_line)
# Unir las líneas preservando los saltos de línea # Unir las líneas preservando los saltos de línea
text = "\n".join(cleaned_lines) text = "\n".join(cleaned_lines)
# Limpiar la combinación específica de CRLF+NBSP+CRLF # Limpiar la combinación específica de
# CRLF + NBSP + CRLF
text = re.sub(r"\r?\n\xa0\r?\n", "\n", text) text = re.sub(r"\r?\n\xa0\r?\n", "\n", text)
# Reemplazar CRLF por LF # Reemplazar CRLF por LF
@ -91,7 +107,10 @@ class MensajeEmail:
""" """
fecha_formato = self.fecha.strftime("%d-%m-%Y") fecha_formato = self.fecha.strftime("%d-%m-%Y")
subject_link = self._formatear_subject_para_link(self.subject) subject_link = self._formatear_subject_para_link(self.subject)
return f"- {fecha_formato} - {self.remitente} - [[cronologia#{self.subject}|{subject_link}]]" return (
f"- {fecha_formato} - {self.remitente} - [[cronologia#"
f"{self.subject}|{subject_link}]]"
)
def _estandarizar_remitente(self, remitente): def _estandarizar_remitente(self, remitente):
if "Da:" in remitente: if "Da:" in remitente:
@ -103,7 +122,8 @@ class MensajeEmail:
if not nombre and email: if not nombre and email:
nombre = email.split("@")[0] nombre = email.split("@")[0]
elif not nombre and not email: elif not nombre and not email:
nombre_match = re.search(r"([A-Za-z\s]+)\s*<", remitente) patron_nombre = r"([A-Za-z\s]+)\s*<"
nombre_match = re.search(patron_nombre, remitente)
if nombre_match: if nombre_match:
nombre = nombre_match.group(1) nombre = nombre_match.group(1)
else: else:
@ -117,17 +137,16 @@ class MensajeEmail:
if isinstance(fecha, str): if isinstance(fecha, str):
try: try:
return parsedate_to_datetime(fecha) return parsedate_to_datetime(fecha)
except: except Exception:
return datetime.now() return datetime.now()
return fecha return fecha
def _generar_hash(self): def _generar_hash(self):
""" """
Genera un hash único para el mensaje basado en una combinación de campos Genera un hash único para el mensaje basado en una combinación de
que identifican únicamente el mensaje campos que identifican únicamente el mensaje
""" """
# Limpiar y normalizar el contenido para el hash # Limpiar y normalizar el contenido para el hash (normaliza espacios)
# Para el hash, sí normalizamos completamente los espacios
contenido_hash = re.sub(r"\s+", " ", self.contenido).strip() contenido_hash = re.sub(r"\s+", " ", self.contenido).strip()
# Normalizar el subject # Normalizar el subject
@ -138,13 +157,11 @@ class MensajeEmail:
# Crear una cadena con los elementos clave del mensaje # Crear una cadena con los elementos clave del mensaje
elementos_hash = [ elementos_hash = [
self.remitente.strip(), self.remitente.strip(),
self.fecha.strftime( # Solo hasta minutos para permitir pequeñas variaciones
"%Y%m%d%H%M" self.fecha.strftime("%Y%m%d%H%M"),
), # Solo hasta minutos para permitir pequeñas variaciones
subject_normalizado, subject_normalizado,
contenido_hash[ # Usar solo los primeros 500 caracteres del contenido normalizado
:500 contenido_hash[:500],
], # Usar solo los primeros 500 caracteres del contenido normalizado
] ]
# Unir todos los elementos con un separador único # Unir todos los elementos con un separador único
@ -152,12 +169,13 @@ class MensajeEmail:
# Mostrar información de debug para el hash (solo si está habilitado) # Mostrar información de debug para el hash (solo si está habilitado)
if hasattr(self, "_debug_hash") and self._debug_hash: if hasattr(self, "_debug_hash") and self._debug_hash:
print(f" 🔍 Debug Hash:") print(" 🔍 Debug Hash:")
print(f" - Remitente: '{self.remitente.strip()}'") print(" - Remitente: '" + self.remitente.strip() + "'")
print(f" - Fecha: '{self.fecha.strftime('%Y%m%d%H%M')}'") print(" - Fecha: '" + self.fecha.strftime("%Y%m%d%H%M") + "'")
print(f" - Subject: '{subject_normalizado}'") print(" - Subject: '" + subject_normalizado + "'")
print(f" - Contenido (500 chars): '{contenido_hash[:500]}'") preview = contenido_hash[:500]
print(f" - Texto completo hash: '{texto_hash[:100]}...'") print(" - Contenido (500 chars): '" + preview + "'")
print(" - Texto completo hash: '" + texto_hash[:100] + "...'")
# Generar el hash # Generar el hash
hash_resultado = hashlib.md5(texto_hash.encode()).hexdigest() hash_resultado = hashlib.md5(texto_hash.encode()).hexdigest()
@ -166,7 +184,8 @@ class MensajeEmail:
def debug_hash_info(self): def debug_hash_info(self):
""" """
Muestra información detallada sobre cómo se genera el hash de este mensaje Muestra información detallada de cómo se genera el hash de este
mensaje
""" """
self._debug_hash = True self._debug_hash = True
hash_result = self._generar_hash() hash_result = self._generar_hash()

View File

@ -3,31 +3,73 @@ import os
import hashlib import hashlib
import re import re
def _contenido_hash(parte):
contenido = parte.get_payload(decode=True) or b""
return hashlib.md5(contenido).hexdigest()
def guardar_adjunto(parte, dir_adjuntos): def guardar_adjunto(parte, dir_adjuntos):
nombre = parte.get_filename() nombre = parte.get_filename()
if not nombre: if not nombre:
return None return None
nombre = re.sub(r'[<>:"/\\|?*]', '_', nombre) nombre = re.sub(r'[<>:"/\\|?*]', "_", nombre)
ruta = os.path.join(dir_adjuntos, nombre) ruta = os.path.join(dir_adjuntos, nombre)
if os.path.exists(ruta): if os.path.exists(ruta):
contenido_nuevo = parte.get_payload(decode=True) hash_nuevo = _contenido_hash(parte)
hash_nuevo = hashlib.md5(contenido_nuevo).hexdigest() with open(ruta, "rb") as f:
with open(ruta, 'rb') as f:
hash_existente = hashlib.md5(f.read()).hexdigest() hash_existente = hashlib.md5(f.read()).hexdigest()
if hash_nuevo == hash_existente: if hash_nuevo == hash_existente:
return ruta return ruta
base, ext = os.path.splitext(nombre) base, ext = os.path.splitext(nombre)
i = 1 i = 1
while os.path.exists(ruta): while os.path.exists(ruta):
ruta = os.path.join(dir_adjuntos, f"{base}_{i}{ext}") ruta = os.path.join(dir_adjuntos, f"{base}_{i}{ext}")
i += 1 i += 1
with open(ruta, 'wb') as f: with open(ruta, "wb") as f:
f.write(parte.get_payload(decode=True))
return ruta
def guardar_imagen(parte, dir_adjuntos):
"""
Guarda una imagen (inline o adjunta). Si no tiene filename, genera uno
basado en Content-ID o hash, preservando la extensión según el subtype.
Devuelve la ruta completa del archivo guardado.
"""
nombre = parte.get_filename()
if not nombre:
# Intentar usar Content-ID
content_id = parte.get("Content-ID", "") or parte.get("Content-Id", "")
content_id = content_id.strip("<>") if content_id else ""
ext = f".{parte.get_content_subtype() or 'bin'}"
base = (
re.sub(r"[^\w\-]+", "_", content_id)
if content_id
else _contenido_hash(parte)
)
nombre = f"img_{base}{ext}"
nombre = re.sub(r'[<>:"/\\|?*]', "_", nombre)
ruta = os.path.join(dir_adjuntos, nombre)
if os.path.exists(ruta):
hash_nuevo = _contenido_hash(parte)
with open(ruta, "rb") as f:
hash_existente = hashlib.md5(f.read()).hexdigest()
if hash_nuevo == hash_existente:
return ruta
base, ext = os.path.splitext(nombre)
i = 1
while os.path.exists(ruta):
ruta = os.path.join(dir_adjuntos, f"{base}_{i}{ext}")
i += 1
with open(ruta, "wb") as f:
f.write(parte.get_payload(decode=True)) f.write(parte.get_payload(decode=True))
return ruta return ruta

View File

@ -8,8 +8,7 @@ from pathlib import Path
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from email.utils import parsedate_to_datetime from email.utils import parsedate_to_datetime
from models.mensaje_email import MensajeEmail from models.mensaje_email import MensajeEmail
from utils.attachment_handler import guardar_adjunto from utils.attachment_handler import guardar_adjunto, guardar_imagen
import tempfile
import os import os
@ -74,9 +73,45 @@ def _should_skip_line(line):
return any(line.strip().startswith(header) for header in headers_to_skip) return any(line.strip().startswith(header) for header in headers_to_skip)
def _html_a_markdown(html): def _find_vault_root(start_path):
""" """
Convierte contenido HTML a texto markdown, extrayendo el asunto si está presente Busca hacia arriba un directorio que contenga la carpeta hermana '.obsidian'.
Devuelve la ruta del directorio raíz del vault o None si no se encuentra.
"""
current = os.path.abspath(start_path)
if os.path.isfile(current):
current = os.path.dirname(current)
while True:
obsidian_dir = os.path.join(current, ".obsidian")
if os.path.isdir(obsidian_dir):
return current
parent = os.path.dirname(current)
if parent == current:
return None
current = parent
def _ruta_relativa_vault(abs_path):
"""
Convierte una ruta absoluta a una ruta relativa al root del vault Obsidian
si se detecta. Si no se detecta, devuelve la ruta original.
"""
abs_path = os.path.abspath(abs_path)
vault_root = _find_vault_root(abs_path)
if not vault_root:
return abs_path
try:
rel = os.path.relpath(abs_path, vault_root)
# Normalizar separadores a '/'
return rel.replace("\\", "/")
except Exception:
return abs_path
def _html_a_markdown(html, cid_to_link=None):
"""
Convierte contenido HTML a texto markdown, extrayendo el asunto si está
presente
""" """
if html is None: if html is None:
return (None, "") return (None, "")
@ -89,6 +124,17 @@ def _html_a_markdown(html):
soup = BeautifulSoup(html, "html.parser") soup = BeautifulSoup(html, "html.parser")
# Reemplazar imágenes inline referenciadas por cid en su lugar
if cid_to_link:
for img in soup.find_all("img"):
src = img.get("src", "")
if src.startswith("cid:"):
cid = src[4:].strip("<>")
embed_path = cid_to_link.get(cid)
if embed_path:
# Obsidian embed (single '!') con ruta relativa al vault
img.replace_with(soup.new_string(f"![[{embed_path}]]"))
# Procesar tablas # Procesar tablas
for table in soup.find_all("table"): for table in soup.find_all("table"):
try: try:
@ -126,7 +172,8 @@ def _html_a_markdown(html):
rowspan = int(cell.get("rowspan", 1)) rowspan = int(cell.get("rowspan", 1))
colspan = int(cell.get("colspan", 1)) colspan = int(cell.get("colspan", 1))
# Procesar el texto de la celda reemplazando saltos de línea por <br> # Procesar texto de la celda reemplazando saltos de
# línea por <br>
cell_text = cell.get_text().strip() cell_text = cell.get_text().strip()
cell_text = cell_text.replace("\n", "<br>") cell_text = cell_text.replace("\n", "<br>")
cell_text = re.sub( cell_text = re.sub(
@ -134,17 +181,16 @@ def _html_a_markdown(html):
) # Eliminar <br> múltiples ) # Eliminar <br> múltiples
cell_text = cell_text.strip() cell_text = cell_text.strip()
# Rellenar la matriz con el texto y None para las celdas combinadas # Rellenar la matriz con el texto y None para celdas
# combinadas
for r in range(rowspan): for r in range(rowspan):
current_row = row_idx + r current_row = row_idx + r
# Expandir matriz si es necesario # Expandir matriz si es necesario
while len(table_matrix) <= current_row: while len(table_matrix) <= current_row:
table_matrix.append([]) table_matrix.append([])
# Expandir fila si es necesario # Expandir fila si es necesario
while ( while len(table_matrix[current_row]) <= col_idx + colspan - 1:
len(table_matrix[current_row]) <= col_idx + colspan - 1 table_matrix[current_row].append(None)
):
table_matrix[current_row].append(None)
for c in range(colspan): for c in range(colspan):
if r == 0 and c == 0: if r == 0 and c == 0:
@ -198,9 +244,8 @@ def _html_a_markdown(html):
# Reemplazar la tabla HTML con la versión Markdown # Reemplazar la tabla HTML con la versión Markdown
if markdown_table: if markdown_table:
table.replace_with( replacement = "\n" + "\n".join(markdown_table) + "\n"
soup.new_string("\n" + "\n".join(markdown_table) + "\n") table.replace_with(soup.new_string(replacement))
)
except Exception as e: except Exception as e:
print(f"Error procesando tabla: {str(e)}") print(f"Error procesando tabla: {str(e)}")
@ -232,7 +277,7 @@ def _html_a_markdown(html):
return (None, html if html else "") return (None, html if html else "")
def _procesar_email_adjunto(parte, dir_adjuntos): def _procesar_email_adjunto(parte, dir_adjuntos, dir_adjuntos_cronologia=None):
""" """
Procesa un email que viene como adjunto dentro de otro email. Procesa un email que viene como adjunto dentro de otro email.
""" """
@ -246,17 +291,29 @@ def _procesar_email_adjunto(parte, dir_adjuntos):
payload = subparte.get_payload() payload = subparte.get_payload()
if isinstance(payload, list): if isinstance(payload, list):
for msg in payload: for msg in payload:
mensajes.extend(procesar_eml_interno(msg, dir_adjuntos)) mensajes.extend(
procesar_eml_interno(
msg, dir_adjuntos, dir_adjuntos_cronologia
)
)
elif isinstance(payload, email.message.Message): elif isinstance(payload, email.message.Message):
mensajes.extend(procesar_eml_interno(payload, dir_adjuntos)) mensajes.extend(
procesar_eml_interno(
payload, dir_adjuntos, dir_adjuntos_cronologia
)
)
else: else:
# Si no es multipart, intentar procesar como mensaje único # Si no es multipart, intentar procesar como mensaje único
payload = parte.get_payload() payload = parte.get_payload()
if isinstance(payload, list): if isinstance(payload, list):
for msg in payload: for msg in payload:
mensajes.extend(procesar_eml_interno(msg, dir_adjuntos)) mensajes.extend(
procesar_eml_interno(msg, dir_adjuntos, dir_adjuntos_cronologia)
)
elif isinstance(payload, email.message.Message): elif isinstance(payload, email.message.Message):
mensajes.extend(procesar_eml_interno(payload, dir_adjuntos)) mensajes.extend(
procesar_eml_interno(payload, dir_adjuntos, dir_adjuntos_cronologia)
)
return mensajes return mensajes
except Exception as e: except Exception as e:
@ -264,7 +321,7 @@ def _procesar_email_adjunto(parte, dir_adjuntos):
return [] return []
def procesar_eml(ruta_archivo, dir_adjuntos): def procesar_eml(ruta_archivo, dir_adjuntos, dir_adjuntos_cronologia=None):
""" """
Punto de entrada principal para procesar archivos .eml Punto de entrada principal para procesar archivos .eml
""" """
@ -273,7 +330,7 @@ def procesar_eml(ruta_archivo, dir_adjuntos):
with open(ruta_archivo, "rb") as eml: with open(ruta_archivo, "rb") as eml:
mensaje = BytesParser(policy=policy.default).parse(eml) mensaje = BytesParser(policy=policy.default).parse(eml)
mensajes = procesar_eml_interno(mensaje, dir_adjuntos) mensajes = procesar_eml_interno(mensaje, dir_adjuntos, dir_adjuntos_cronologia)
print(f" 📧 Procesamiento completado: {len(mensajes)} mensajes extraídos") print(f" 📧 Procesamiento completado: {len(mensajes)} mensajes extraídos")
return mensajes return mensajes
except Exception as e: except Exception as e:
@ -281,7 +338,7 @@ def procesar_eml(ruta_archivo, dir_adjuntos):
return [] return []
def procesar_eml_interno(mensaje, dir_adjuntos): def procesar_eml_interno(mensaje, dir_adjuntos, dir_adjuntos_cronologia=None):
""" """
Procesa un mensaje de email, ya sea desde archivo o adjunto Procesa un mensaje de email, ya sea desde archivo o adjunto
""" """
@ -296,13 +353,15 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
subject = mensaje.get("subject", "") subject = mensaje.get("subject", "")
if subject: if subject:
# Try to decode if it's encoded # Try to decode if it's encoded
subject = str(email.header.make_header(email.header.decode_header(subject))) decoded = email.header.decode_header(subject)
subject = str(email.header.make_header(decoded))
contenido = "" contenido = ""
adjuntos = [] adjuntos = []
imagenes = []
tiene_html = False tiene_html = False
# First pass: check for HTML content # Primer pase: detectar si hay HTML
if mensaje.is_multipart(): if mensaje.is_multipart():
for parte in mensaje.walk(): for parte in mensaje.walk():
if parte.get_content_type() == "text/html": if parte.get_content_type() == "text/html":
@ -311,10 +370,29 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
else: else:
tiene_html = mensaje.get_content_type() == "text/html" tiene_html = mensaje.get_content_type() == "text/html"
# Second pass: process content and attachments # Segundo pase: procesar contenido y adjuntos
if mensaje.is_multipart(): if mensaje.is_multipart():
# Asegurarnos de capturar SOLO una vez el cuerpo principal y no sobrescribirlo # Capturar SOLO una vez el cuerpo principal y no sobrescribirlo
contenido_set = False # flag para no re-asignar contenido principal contenido_set = False # flag para no re-asignar contenido principal
# Construir mapa cid->ruta de embed (relativa al vault) para inline
cid_to_link = {}
if dir_adjuntos_cronologia:
for parte in mensaje.walk():
ctype = parte.get_content_type()
dispo = parte.get_content_disposition()
if ctype.startswith("image/") and dispo in (None, "inline"):
cid_header = parte.get("Content-ID", "") or parte.get(
"Content-Id", ""
)
if cid_header:
cid_clean = cid_header.strip("<>")
# Guardar SOLO en adjuntos/cronologia
ruta_img = guardar_imagen(parte, dir_adjuntos_cronologia)
if ruta_img:
# Convertir a ruta relativa al vault
embed_path = _ruta_relativa_vault(ruta_img)
cid_to_link[cid_clean] = embed_path
for parte in mensaje.walk(): for parte in mensaje.walk():
content_type = parte.get_content_type() content_type = parte.get_content_type()
@ -330,7 +408,9 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
if content_type == "text/html": if content_type == "text/html":
html_content = _get_payload_safely(parte) html_content = _get_payload_safely(parte)
if html_content: if html_content:
part_subject, text = _html_a_markdown(html_content) part_subject, text = _html_a_markdown(
html_content, cid_to_link
)
if not subject and part_subject: if not subject and part_subject:
subject = part_subject subject = part_subject
if text: if text:
@ -346,7 +426,9 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
# 2. EMAILS RFC822 ADJUNTOS # 2. EMAILS RFC822 ADJUNTOS
# ----------------------------- # -----------------------------
elif content_type == "message/rfc822": elif content_type == "message/rfc822":
mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos) mensajes_adjuntos = _procesar_email_adjunto(
parte, dir_adjuntos, dir_adjuntos_cronologia
)
mensajes.extend(mensajes_adjuntos) mensajes.extend(mensajes_adjuntos)
# ----------------------------- # -----------------------------
@ -356,13 +438,27 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
nombre = parte.get_filename() nombre = parte.get_filename()
if nombre and nombre.lower().endswith(".eml"): if nombre and nombre.lower().endswith(".eml"):
mensajes_adjuntos = _procesar_email_adjunto( mensajes_adjuntos = _procesar_email_adjunto(
parte, dir_adjuntos parte, dir_adjuntos, dir_adjuntos_cronologia
) )
mensajes.extend(mensajes_adjuntos) mensajes.extend(mensajes_adjuntos)
else: else:
ruta_adjunto = guardar_adjunto(parte, dir_adjuntos) # Imagen adjunta (no inline): solo guardar en adjuntos
if ruta_adjunto: if content_type.startswith("image/"):
adjuntos.append(Path(ruta_adjunto).name) ruta_img = guardar_imagen(parte, dir_adjuntos)
if ruta_img:
adjuntos.append(Path(ruta_img).name)
else:
ruta_adjunto = guardar_adjunto(parte, dir_adjuntos)
if ruta_adjunto:
adjuntos.append(Path(ruta_adjunto).name)
# 4. IMÁGENES INLINE: ya manejadas para embebido; no listar
elif content_type.startswith("image/") and (
parte.get_content_disposition() in (None, "inline")
):
# Nada que hacer aquí; ya se guardó en cronologia y
# se reemplazó en el HTML
pass
except Exception as e: except Exception as e:
print(f"Error procesando parte del mensaje: {str(e)}") print(f"Error procesando parte del mensaje: {str(e)}")
@ -371,7 +467,8 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
if mensaje.get_content_type() == "text/html": if mensaje.get_content_type() == "text/html":
html_content = _get_payload_safely(mensaje) html_content = _get_payload_safely(mensaje)
if html_content: if html_content:
part_subject, contenido = _html_a_markdown(html_content) # Para mensajes no multipart, no hay inline cid a resolver
part_subject, contenido = _html_a_markdown(html_content, {})
if not subject and part_subject: if not subject and part_subject:
subject = part_subject subject = part_subject
else: else:
@ -386,7 +483,7 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
subject=subject, subject=subject,
adjuntos=adjuntos, adjuntos=adjuntos,
) )
print(f" ✉️ Mensaje extraído:") print(" ✉️ Mensaje extraído:")
print(f" - Subject: {subject}") print(f" - Subject: {subject}")
print(f" - Remitente: {remitente}") print(f" - Remitente: {remitente}")
print(f" - Fecha: {fecha}") print(f" - Fecha: {fecha}")
@ -395,7 +492,7 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
print(f" - Hash generado: {mensaje_nuevo.hash}") print(f" - Hash generado: {mensaje_nuevo.hash}")
mensajes.append(mensaje_nuevo) mensajes.append(mensaje_nuevo)
else: else:
print(f" ⚠️ Mensaje vacío o sin contenido útil - no se agregará") print(" ⚠️ Mensaje vacío o sin contenido útil - no se agregará")
except Exception as e: except Exception as e:
print(f"Error procesando mensaje: {str(e)}") print(f"Error procesando mensaje: {str(e)}")
@ -407,7 +504,7 @@ def _parsear_fecha(fecha_str):
try: try:
fecha = parsedate_to_datetime(fecha_str) fecha = parsedate_to_datetime(fecha_str)
return fecha.replace(tzinfo=None) # Remove timezone info return fecha.replace(tzinfo=None) # Remove timezone info
except: except Exception:
try: try:
fecha_match = re.search( fecha_match = re.search(
r"venerd=EC (\d{1,2}) (\w+) (\d{4}) (\d{1,2}):(\d{2})", fecha_str r"venerd=EC (\d{1,2}) (\w+) (\d{4}) (\d{1,2}):(\d{2})", fecha_str
@ -430,6 +527,6 @@ def _parsear_fecha(fecha_str):
} }
mes_num = meses_it.get(mes.lower(), 1) mes_num = meses_it.get(mes.lower(), 1)
return datetime(int(año), mes_num, int(dia), int(hora), int(minuto)) return datetime(int(año), mes_num, int(dia), int(hora), int(minuto))
except: except Exception:
pass pass
return datetime.now() return datetime.now()

View File

@ -62,6 +62,7 @@ def main():
# Construir rutas de salida en working_directory # Construir rutas de salida en working_directory
output_file = os.path.join(working_directory, cronologia_file) output_file = os.path.join(working_directory, cronologia_file)
attachments_path = os.path.join(working_directory, attachments_dir) attachments_path = os.path.join(working_directory, attachments_dir)
attachments_crono_path = os.path.join(attachments_path, "cronologia")
# Debug prints # Debug prints
print(f"Working/Output directory: {working_directory}") print(f"Working/Output directory: {working_directory}")
@ -80,6 +81,7 @@ def main():
# Asegurar directorios de salida # Asegurar directorios de salida
os.makedirs(working_directory, exist_ok=True) os.makedirs(working_directory, exist_ok=True)
os.makedirs(attachments_path, exist_ok=True) os.makedirs(attachments_path, exist_ok=True)
os.makedirs(attachments_crono_path, exist_ok=True)
# Check if input directory exists and has files # Check if input directory exists and has files
input_path = Path(input_dir) input_path = Path(input_dir)
@ -110,7 +112,9 @@ def main():
print(f"\n{'='*60}") print(f"\n{'='*60}")
print(f"Processing file: {archivo}") print(f"Processing file: {archivo}")
sys.stdout.flush() sys.stdout.flush()
nuevos_mensajes = procesar_eml(archivo, attachments_path) nuevos_mensajes = procesar_eml(
archivo, attachments_path, attachments_crono_path
)
print(f"Extracted {len(nuevos_mensajes)} messages from {archivo.name}") print(f"Extracted {len(nuevos_mensajes)} messages from {archivo.name}")
sys.stdout.flush() sys.stdout.flush()
total_procesados += len(nuevos_mensajes) total_procesados += len(nuevos_mensajes)

View File

@ -135,6 +135,21 @@
position: relative; position: relative;
} }
select {
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1em;
background: white;
transition: border-color 0.3s ease;
}
select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
input[type="text"] { input[type="text"] {
padding: 12px 16px; padding: 12px 16px;
border: 2px solid #e2e8f0; border: 2px solid #e2e8f0;
@ -461,6 +476,18 @@
<button class="btn btn-secondary" onclick="refreshModels()"> <button class="btn btn-secondary" onclick="refreshModels()">
🔄 Actualizar 🔄 Actualizar
</button> </button>
<div class="input-group">
<label for="sort-select"
style="margin-left:10px; margin-right:6px; color:#4a5568; font-weight:600;">Ordenar
por:</label>
<select id="sort-select">
<option value="modified_desc">Última modificación (recientes primero)</option>
<option value="modified_asc">Última modificación (antiguos primero)</option>
<option value="size_desc">Tamaño (grandes primero)</option>
<option value="size_asc">Tamaño (pequeños primero)</option>
<option value="name_asc">Nombre (A-Z)</option>
</select>
</div>
</div> </div>
</div> </div>
@ -497,6 +524,7 @@
// Estado global de la aplicación // Estado global de la aplicación
let currentModels = []; let currentModels = [];
let isLoading = false; let isLoading = false;
let currentSort = 'modified_desc';
// Inicializar aplicación // Inicializar aplicación
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
@ -509,6 +537,16 @@
downloadModel(); downloadModel();
} }
}); });
// Configurar selector de orden
const sortSelect = document.getElementById('sort-select');
if (sortSelect) {
sortSelect.value = currentSort;
sortSelect.addEventListener('change', function () {
currentSort = this.value;
applySortAndRender();
});
}
}); });
// Verificar estado de Ollama // Verificar estado de Ollama
@ -557,7 +595,7 @@
if (data.status === 'success') { if (data.status === 'success') {
currentModels = data.models || []; currentModels = data.models || [];
updateStats(data); updateStats(data);
renderModels(currentModels); applySortAndRender();
} else { } else {
showError(`Error al cargar modelos: ${data.message}`); showError(`Error al cargar modelos: ${data.message}`);
container.innerHTML = ` container.innerHTML = `
@ -578,6 +616,34 @@
} }
} }
// Aplicar orden y renderizar
function applySortAndRender() {
const sorted = [...currentModels];
const getSize = (m) => typeof m.size === 'number' ? m.size : (parseInt(m.size, 10) || 0);
const getModifiedTs = (m) => m.modified_at ? (Date.parse(m.modified_at) || 0) : 0;
switch (currentSort) {
case 'size_desc':
sorted.sort((a, b) => getSize(b) - getSize(a));
break;
case 'size_asc':
sorted.sort((a, b) => getSize(a) - getSize(b));
break;
case 'modified_asc':
sorted.sort((a, b) => getModifiedTs(a) - getModifiedTs(b));
break;
case 'name_asc':
sorted.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
break;
case 'modified_desc':
default:
sorted.sort((a, b) => getModifiedTs(b) - getModifiedTs(a));
break;
}
renderModels(sorted);
}
// Actualizar estadísticas // Actualizar estadísticas
function updateStats(data) { function updateStats(data) {
const statsElement = document.getElementById('stats'); const statsElement = document.getElementById('stats');

View File

@ -1,5 +1,31 @@
{ {
"history": [ "history": [
{
"id": "286de385",
"group_id": "2",
"script_name": "main.py",
"executed_date": "2025-08-08T13:31:57.810744Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 63664,
"execution_time": null
},
{
"id": "19b3a39f",
"group_id": "2",
"script_name": "main.py",
"executed_date": "2025-08-08T12:08:20.490182Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 66300,
"execution_time": null
},
{ {
"id": "23248897", "id": "23248897",
"group_id": "2", "group_id": "2",

View File

@ -1,107 +1,107 @@
[11:43:49] Iniciando ejecución de x1.py en C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098... [14:55:47] Iniciando ejecución de x1.py en C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098...
[11:43:49] ✅ Configuración cargada exitosamente [14:55:47] ✅ Configuración cargada exitosamente
[11:43:49] Working/Output directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098 [14:55:47] Working/Output directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098
[11:43:49] Input directory: C:\Trabajo\SIDEL\17 - E5.006880 - Modifica O&U - RSC098\Reporte\Emails [14:55:47] Input directory: C:\Trabajo\SIDEL\17 - E5.006880 - Modifica O&U - RSC098\Reporte\Emails
[11:43:49] Output file: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\cronologia.md [14:55:47] Output file: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\cronologia.md
[11:43:49] Attachments directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\adjuntos [14:55:47] Attachments directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\adjuntos
[11:43:49] Beautify rules file: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\config\beautify_rules.json [14:55:47] Beautify rules file: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\config\beautify_rules.json
[11:43:49] Found 1 .eml files [14:55:47] Found 1 .eml files
[11:43:49] Creando cronología nueva (archivo se sobrescribirá) [14:55:47] Creando cronología nueva (archivo se sobrescribirá)
[11:43:49] ============================================================ [14:55:47] ============================================================
[11:43:49] Processing file: C:\Trabajo\SIDEL\17 - E5.006880 - Modifica O&U - RSC098\Reporte\Emails\R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml [14:55:47] Processing file: C:\Trabajo\SIDEL\17 - E5.006880 - Modifica O&U - RSC098\Reporte\Emails\R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml
[11:43:49] 📧 Abriendo archivo: C:\Trabajo\SIDEL\17 - E5.006880 - Modifica O&U - RSC098\Reporte\Emails\R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml [14:55:47] 📧 Abriendo archivo: C:\Trabajo\SIDEL\17 - E5.006880 - Modifica O&U - RSC098\Reporte\Emails\R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml
[11:43:49] ✉️ Mensaje extraído: [14:55:48] ✉️ Mensaje extraído:
[11:43:49] - Subject: R: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT [14:55:48] - Subject: R: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT
[11:43:49] - Remitente: "Passera, Alessandro" <Alessandro.Passera@sidel.com> [14:55:48] - Remitente: "Passera, Alessandro" <Alessandro.Passera@sidel.com>
[11:43:49] - Fecha: 2025-08-08 07:49:28 [14:55:48] - Fecha: 2025-08-08 07:49:28
[11:43:49] - Adjuntos: 0 archivos [14:55:48] - Adjuntos: 0 archivos
[11:43:49] - Contenido: 4735 caracteres [14:55:48] - Contenido: 5160 caracteres
[11:43:49] - Hash generado: 48f94bf24945f73bc08c1c0cf8c1e8bb [14:55:48] - Hash generado: 48f94bf24945f73bc08c1c0cf8c1e8bb
[11:43:49] ✉️ Mensaje extraído: [14:55:48] ✉️ Mensaje extraído:
[11:43:49] - Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT [14:55:48] - Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT
[11:43:49] - Remitente: "Bii, Vickodyne" <vickodyne.bii@sidel.com> [14:55:48] - Remitente: "Bii, Vickodyne" <vickodyne.bii@sidel.com>
[11:43:49] - Fecha: 2025-08-08 05:46:30 [14:55:48] - Fecha: 2025-08-08 05:46:30
[11:43:49] - Adjuntos: 0 archivos [14:55:48] - Adjuntos: 0 archivos
[11:43:49] - Contenido: 4259 caracteres [14:55:48] - Contenido: 4686 caracteres
[11:43:49] - Hash generado: 82709d4677b90d79bb02e13cfe86924e [14:55:48] - Hash generado: 352a3d37ac274b11822ca5527cd8865b
[11:43:49] ✉️ Mensaje extraído: [14:55:48] ✉️ Mensaje extraído:
[11:43:49] - Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT [14:55:48] - Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT
[11:43:49] - Remitente: "walter.orsi@teknors.com" <walter.orsi@teknors.com> [14:55:48] - Remitente: "walter.orsi@teknors.com" <walter.orsi@teknors.com>
[11:43:49] - Fecha: 2025-08-07 15:55:58 [14:55:48] - Fecha: 2025-08-07 15:55:58
[11:43:49] - Adjuntos: 0 archivos [14:55:48] - Adjuntos: 0 archivos
[11:43:49] - Contenido: 3235 caracteres [14:55:48] - Contenido: 3583 caracteres
[11:43:49] - Hash generado: cceec9818de1a4491214af4b6d96e143 [14:55:48] - Hash generado: b2558be8631ba7d14210a4a3379dfdad
[11:43:49] ✉️ Mensaje extraído: [14:55:48] ✉️ Mensaje extraído:
[11:43:49] - Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT [14:55:48] - Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT
[11:43:49] - Remitente: "Passera, Alessandro" <Alessandro.Passera@sidel.com> [14:55:48] - Remitente: "Passera, Alessandro" <Alessandro.Passera@sidel.com>
[11:43:49] - Fecha: 2025-08-07 13:07:11 [14:55:48] - Fecha: 2025-08-07 13:07:11
[11:43:49] - Adjuntos: 0 archivos [14:55:48] - Adjuntos: 0 archivos
[11:43:49] - Contenido: 2398 caracteres [14:55:48] - Contenido: 2485 caracteres
[11:43:49] - Hash generado: dc05b2959920f679cd60e8a29685badc [14:55:48] - Hash generado: dc05b2959920f679cd60e8a29685badc
[11:43:49] ✉️ Mensaje extraído: [14:55:48] ✉️ Mensaje extraído:
[11:43:49] - Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT [14:55:48] - Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT
[11:43:49] - Remitente: "Passera, Alessandro" <Alessandro.Passera@sidel.com> [14:55:48] - Remitente: "Passera, Alessandro" <Alessandro.Passera@sidel.com>
[11:43:49] - Fecha: 2025-08-07 12:59:15 [14:55:48] - Fecha: 2025-08-07 12:59:15
[11:43:49] - Adjuntos: 0 archivos [14:55:48] - Adjuntos: 0 archivos
[11:43:49] - Contenido: 1613 caracteres [14:55:48] - Contenido: 1700 caracteres
[11:43:49] - Hash generado: a848be3351ae2cc44bafb0f322a78690 [14:55:48] - Hash generado: a848be3351ae2cc44bafb0f322a78690
[11:43:49] ✉️ Mensaje extraído: [14:55:48] ✉️ Mensaje extraído:
[11:43:49] - Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT [14:55:48] - Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT
[11:43:49] - Remitente: Miguel Angel Vera <miguelverateknors@gmail.com> [14:55:48] - Remitente: Miguel Angel Vera <miguelverateknors@gmail.com>
[11:43:49] - Fecha: 2025-08-08 09:41:58 [14:55:48] - Fecha: 2025-08-08 09:41:58
[11:43:49] - Adjuntos: 0 archivos [14:55:48] - Adjuntos: 0 archivos
[11:43:49] - Contenido: 4735 caracteres [14:55:48] - Contenido: 5160 caracteres
[11:43:49] - Hash generado: 430cc918020c3c8db795995baa26cb78 [14:55:48] - Hash generado: 430cc918020c3c8db795995baa26cb78
[11:43:49] 📧 Procesamiento completado: 6 mensajes extraídos [14:55:48] 📧 Procesamiento completado: 6 mensajes extraídos
[11:43:49] Extracted 6 messages from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml [14:55:48] Extracted 6 messages from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml
[11:43:49] --- Msg 1/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- [14:55:48] --- Msg 1/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml ---
[11:43:49] Remitente: Passera, Alessandro [14:55:48] Remitente: Passera, Alessandro
[11:43:49] Fecha: 2025-08-08 07:49:28 [14:55:48] Fecha: 2025-08-08 07:49:28
[11:43:49] Subject: R: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT [14:55:48] Subject: R: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT
[11:43:49] Hash: 48f94bf24945f73bc08c1c0cf8c1e8bb [14:55:48] Hash: 48f94bf24945f73bc08c1c0cf8c1e8bb
[11:43:49] Adjuntos: [] [14:55:48] Adjuntos: []
[11:43:49] ✓ NUEVO mensaje - Agregando a la cronología [14:55:48] ✓ NUEVO mensaje - Agregando a la cronología
[11:43:49] --- Msg 2/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- [14:55:48] --- Msg 2/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml ---
[11:43:49] Remitente: Bii, Vickodyne [14:55:48] Remitente: Bii, Vickodyne
[11:43:49] Fecha: 2025-08-08 05:46:30 [14:55:48] Fecha: 2025-08-08 05:46:30
[11:43:49] Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT [14:55:48] Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT
[11:43:49] Hash: 82709d4677b90d79bb02e13cfe86924e [14:55:48] Hash: 352a3d37ac274b11822ca5527cd8865b
[11:43:49] Adjuntos: [] [14:55:48] Adjuntos: []
[11:43:49] ✓ NUEVO mensaje - Agregando a la cronología [14:55:48] ✓ NUEVO mensaje - Agregando a la cronología
[11:43:49] --- Msg 3/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- [14:55:48] --- Msg 3/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml ---
[11:43:49] Remitente: walter.orsi@teknors.com [14:55:48] Remitente: walter.orsi@teknors.com
[11:43:49] Fecha: 2025-08-07 15:55:58 [14:55:48] Fecha: 2025-08-07 15:55:58
[11:43:49] Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT [14:55:48] Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT
[11:43:49] Hash: cceec9818de1a4491214af4b6d96e143 [14:55:48] Hash: b2558be8631ba7d14210a4a3379dfdad
[11:43:49] Adjuntos: [] [14:55:48] Adjuntos: []
[11:43:49] ✓ NUEVO mensaje - Agregando a la cronología [14:55:48] ✓ NUEVO mensaje - Agregando a la cronología
[11:43:49] --- Msg 4/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- [14:55:48] --- Msg 4/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml ---
[11:43:49] Remitente: Passera, Alessandro [14:55:48] Remitente: Passera, Alessandro
[11:43:49] Fecha: 2025-08-07 13:07:11 [14:55:48] Fecha: 2025-08-07 13:07:11
[11:43:49] Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT [14:55:48] Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT
[11:43:49] Hash: dc05b2959920f679cd60e8a29685badc [14:55:48] Hash: dc05b2959920f679cd60e8a29685badc
[11:43:49] Adjuntos: [] [14:55:48] Adjuntos: []
[11:43:49] ✓ NUEVO mensaje - Agregando a la cronología [14:55:48] ✓ NUEVO mensaje - Agregando a la cronología
[11:43:49] --- Msg 5/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- [14:55:48] --- Msg 5/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml ---
[11:43:49] Remitente: Passera, Alessandro [14:55:48] Remitente: Passera, Alessandro
[11:43:49] Fecha: 2025-08-07 12:59:15 [14:55:48] Fecha: 2025-08-07 12:59:15
[11:43:49] Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT [14:55:48] Subject: R: E5.006880 - RSC098 - Nigerian Breweries: URGENT
[11:43:49] Hash: a848be3351ae2cc44bafb0f322a78690 [14:55:48] Hash: a848be3351ae2cc44bafb0f322a78690
[11:43:49] Adjuntos: [] [14:55:48] Adjuntos: []
[11:43:49] ✓ NUEVO mensaje - Agregando a la cronología [14:55:48] ✓ NUEVO mensaje - Agregando a la cronología
[11:43:49] --- Msg 6/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml --- [14:55:48] --- Msg 6/6 from R_ E5.006880 - RSC098 - Nigerian Breweries_ URGENT.eml ---
[11:43:49] Remitente: Miguel Angel Vera [14:55:48] Remitente: Miguel Angel Vera
[11:43:49] Fecha: 2025-08-08 09:41:58 [14:55:48] Fecha: 2025-08-08 09:41:58
[11:43:49] Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT [14:55:48] Subject: RE: {EXT} R: E5.006880 - RSC098 - Nigerian Breweries: URGENT
[11:43:49] Hash: 430cc918020c3c8db795995baa26cb78 [14:55:48] Hash: 430cc918020c3c8db795995baa26cb78
[11:43:49] Adjuntos: [] [14:55:48] Adjuntos: []
[11:43:49] ✓ NUEVO mensaje - Agregando a la cronología [14:55:48] ✓ NUEVO mensaje - Agregando a la cronología
[11:43:49] Estadísticas de procesamiento: [14:55:48] Estadísticas de procesamiento:
[11:43:49] - Total mensajes encontrados: 6 [14:55:48] - Total mensajes encontrados: 6
[11:43:49] - Mensajes únicos añadidos: 6 [14:55:48] - Mensajes únicos añadidos: 6
[11:43:49] - Mensajes duplicados ignorados: 0 [14:55:48] - Mensajes duplicados ignorados: 0
[11:43:49] Writing 6 messages to C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\cronologia.md [14:55:48] Writing 6 messages to C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\cronologia.md
[11:43:49] ✅ Cronología guardada exitosamente en: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\cronologia.md [14:55:48] ✅ Cronología guardada exitosamente en: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\17 - E5.006880 - Modifica O&U - RSC098\cronologia.md
[11:43:49] 📊 Total de mensajes en la cronología: 6 [14:55:48] 📊 Total de mensajes en la cronología: 6
[11:43:49] Ejecución de x1.py finalizada (success). Duración: 0:00:00.353973. [14:55:48] Ejecución de x1.py finalizada (success). Duración: 0:00:00.497219.
[11:43:49] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\.log\log_x1.txt [14:55:48] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\.log\log_x1.txt