diff --git a/backend/script_groups/EmailCrono/.doc/MemoriaDeEvolucion.md b/backend/script_groups/EmailCrono/.doc/MemoriaDeEvolucion.md
index b8da862..92034fb 100644
--- a/backend/script_groups/EmailCrono/.doc/MemoriaDeEvolucion.md
+++ b/backend/script_groups/EmailCrono/.doc/MemoriaDeEvolucion.md
@@ -38,4 +38,21 @@ Salida Markdown: Escribe el índice seguido del contenido formateado en Markdown
- Se corrigen lints (E402 y líneas largas) sin alterar lógica.
- Impacto:
- Los `work_dir.json` existentes deben actualizar la clave a `input_directory`.
- - No hay cambios en claves de `level2` (`cronologia_file`, `attachments_dir`).
\ No newline at end of file
+ - 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]]`.
\ No newline at end of file
diff --git a/backend/script_groups/EmailCrono/models/mensaje_email.py b/backend/script_groups/EmailCrono/models/mensaje_email.py
index fb7335b..31520f6 100644
--- a/backend/script_groups/EmailCrono/models/mensaje_email.py
+++ b/backend/script_groups/EmailCrono/models/mensaje_email.py
@@ -6,7 +6,14 @@ from email.utils import parseaddr, parsedate_to_datetime
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.fecha = self._estandarizar_fecha(fecha)
self.subject = subject if subject else "Sin Asunto"
@@ -37,17 +44,26 @@ class MensajeEmail:
for line in lines:
# Skip metadata lines
if line.strip().startswith(
- ("Da: ", "Inviato: ", "A: ", "From: ", "Sent: ", "To: ")
+ (
+ "Da: ",
+ "Inviato: ",
+ "A: ",
+ "From: ",
+ "Sent: ",
+ "To: ",
+ )
) or line.strip().startswith("Oggetto: "):
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_lines.append(cleaned_line)
# Unir las líneas preservando los saltos de línea
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)
# Reemplazar CRLF por LF
@@ -91,7 +107,10 @@ class MensajeEmail:
"""
fecha_formato = self.fecha.strftime("%d-%m-%Y")
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):
if "Da:" in remitente:
@@ -103,7 +122,8 @@ class MensajeEmail:
if not nombre and email:
nombre = email.split("@")[0]
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:
nombre = nombre_match.group(1)
else:
@@ -117,17 +137,16 @@ class MensajeEmail:
if isinstance(fecha, str):
try:
return parsedate_to_datetime(fecha)
- except:
+ except Exception:
return datetime.now()
return fecha
def _generar_hash(self):
"""
- Genera un hash único para el mensaje basado en una combinación de campos
- que identifican únicamente el mensaje
+ Genera un hash único para el mensaje basado en una combinación de
+ campos que identifican únicamente el mensaje
"""
- # Limpiar y normalizar el contenido para el hash
- # Para el hash, sí normalizamos completamente los espacios
+ # Limpiar y normalizar el contenido para el hash (normaliza espacios)
contenido_hash = re.sub(r"\s+", " ", self.contenido).strip()
# Normalizar el subject
@@ -138,13 +157,11 @@ class MensajeEmail:
# Crear una cadena con los elementos clave del mensaje
elementos_hash = [
self.remitente.strip(),
- self.fecha.strftime(
- "%Y%m%d%H%M"
- ), # Solo hasta minutos para permitir pequeñas variaciones
+ # Solo hasta minutos para permitir pequeñas variaciones
+ self.fecha.strftime("%Y%m%d%H%M"),
subject_normalizado,
- contenido_hash[
- :500
- ], # Usar solo los primeros 500 caracteres del contenido normalizado
+ # Usar solo los primeros 500 caracteres del contenido normalizado
+ contenido_hash[:500],
]
# 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)
if hasattr(self, "_debug_hash") and self._debug_hash:
- print(f" 🔍 Debug Hash:")
- print(f" - Remitente: '{self.remitente.strip()}'")
- print(f" - Fecha: '{self.fecha.strftime('%Y%m%d%H%M')}'")
- print(f" - Subject: '{subject_normalizado}'")
- print(f" - Contenido (500 chars): '{contenido_hash[:500]}'")
- print(f" - Texto completo hash: '{texto_hash[:100]}...'")
+ print(" 🔍 Debug Hash:")
+ print(" - Remitente: '" + self.remitente.strip() + "'")
+ print(" - Fecha: '" + self.fecha.strftime("%Y%m%d%H%M") + "'")
+ print(" - Subject: '" + subject_normalizado + "'")
+ preview = contenido_hash[:500]
+ print(" - Contenido (500 chars): '" + preview + "'")
+ print(" - Texto completo hash: '" + texto_hash[:100] + "...'")
# Generar el hash
hash_resultado = hashlib.md5(texto_hash.encode()).hexdigest()
@@ -166,7 +184,8 @@ class MensajeEmail:
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
hash_result = self._generar_hash()
diff --git a/backend/script_groups/EmailCrono/utils/attachment_handler.py b/backend/script_groups/EmailCrono/utils/attachment_handler.py
index 80679cb..946e376 100644
--- a/backend/script_groups/EmailCrono/utils/attachment_handler.py
+++ b/backend/script_groups/EmailCrono/utils/attachment_handler.py
@@ -3,31 +3,73 @@ import os
import hashlib
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):
nombre = parte.get_filename()
if not nombre:
return None
- nombre = re.sub(r'[<>:"/\\|?*]', '_', nombre)
+ nombre = re.sub(r'[<>:"/\\|?*]', "_", nombre)
ruta = os.path.join(dir_adjuntos, nombre)
if os.path.exists(ruta):
- contenido_nuevo = parte.get_payload(decode=True)
- hash_nuevo = hashlib.md5(contenido_nuevo).hexdigest()
-
- with open(ruta, 'rb') as f:
+ 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:
+ 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))
+
return ruta
diff --git a/backend/script_groups/EmailCrono/utils/email_parser.py b/backend/script_groups/EmailCrono/utils/email_parser.py
index 1195c85..e3d0af5 100644
--- a/backend/script_groups/EmailCrono/utils/email_parser.py
+++ b/backend/script_groups/EmailCrono/utils/email_parser.py
@@ -8,8 +8,7 @@ from pathlib import Path
from bs4 import BeautifulSoup
from email.utils import parsedate_to_datetime
from models.mensaje_email import MensajeEmail
-from utils.attachment_handler import guardar_adjunto
-import tempfile
+from utils.attachment_handler import guardar_adjunto, guardar_imagen
import os
@@ -74,9 +73,45 @@ def _should_skip_line(line):
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:
return (None, "")
@@ -89,6 +124,17 @@ def _html_a_markdown(html):
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
for table in soup.find_all("table"):
try:
@@ -126,7 +172,8 @@ def _html_a_markdown(html):
rowspan = int(cell.get("rowspan", 1))
colspan = int(cell.get("colspan", 1))
- # Procesar el texto de la celda reemplazando saltos de línea por
+ # Procesar texto de la celda reemplazando saltos de
+ # línea por
cell_text = cell.get_text().strip()
cell_text = cell_text.replace("\n", "
")
cell_text = re.sub(
@@ -134,17 +181,16 @@ def _html_a_markdown(html):
) # Eliminar
múltiples
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):
current_row = row_idx + r
# Expandir matriz si es necesario
while len(table_matrix) <= current_row:
table_matrix.append([])
- # Expandir fila si es necesario
- while (
- len(table_matrix[current_row]) <= col_idx + colspan - 1
- ):
- table_matrix[current_row].append(None)
+ # Expandir fila si es necesario
+ while len(table_matrix[current_row]) <= col_idx + colspan - 1:
+ table_matrix[current_row].append(None)
for c in range(colspan):
if r == 0 and c == 0:
@@ -198,9 +244,8 @@ def _html_a_markdown(html):
# Reemplazar la tabla HTML con la versión Markdown
if markdown_table:
- table.replace_with(
- soup.new_string("\n" + "\n".join(markdown_table) + "\n")
- )
+ replacement = "\n" + "\n".join(markdown_table) + "\n"
+ table.replace_with(soup.new_string(replacement))
except Exception as e:
print(f"Error procesando tabla: {str(e)}")
@@ -232,7 +277,7 @@ def _html_a_markdown(html):
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.
"""
@@ -246,17 +291,29 @@ def _procesar_email_adjunto(parte, dir_adjuntos):
payload = subparte.get_payload()
if isinstance(payload, list):
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):
- mensajes.extend(procesar_eml_interno(payload, dir_adjuntos))
+ mensajes.extend(
+ procesar_eml_interno(
+ payload, dir_adjuntos, dir_adjuntos_cronologia
+ )
+ )
else:
# Si no es multipart, intentar procesar como mensaje único
payload = parte.get_payload()
if isinstance(payload, list):
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):
- mensajes.extend(procesar_eml_interno(payload, dir_adjuntos))
+ mensajes.extend(
+ procesar_eml_interno(payload, dir_adjuntos, dir_adjuntos_cronologia)
+ )
return mensajes
except Exception as e:
@@ -264,7 +321,7 @@ def _procesar_email_adjunto(parte, dir_adjuntos):
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
"""
@@ -273,7 +330,7 @@ def procesar_eml(ruta_archivo, dir_adjuntos):
with open(ruta_archivo, "rb") as 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")
return mensajes
except Exception as e:
@@ -281,7 +338,7 @@ def procesar_eml(ruta_archivo, dir_adjuntos):
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
"""
@@ -296,13 +353,15 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
subject = mensaje.get("subject", "")
if subject:
# 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 = ""
adjuntos = []
+ imagenes = []
tiene_html = False
- # First pass: check for HTML content
+ # Primer pase: detectar si hay HTML
if mensaje.is_multipart():
for parte in mensaje.walk():
if parte.get_content_type() == "text/html":
@@ -311,10 +370,29 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
else:
tiene_html = mensaje.get_content_type() == "text/html"
- # Second pass: process content and attachments
+ # Segundo pase: procesar contenido y adjuntos
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
+ # 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():
content_type = parte.get_content_type()
@@ -330,7 +408,9 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
if content_type == "text/html":
html_content = _get_payload_safely(parte)
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:
subject = part_subject
if text:
@@ -346,7 +426,9 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
# 2. EMAILS RFC822 ADJUNTOS
# -----------------------------
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)
# -----------------------------
@@ -356,13 +438,27 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
nombre = parte.get_filename()
if nombre and nombre.lower().endswith(".eml"):
mensajes_adjuntos = _procesar_email_adjunto(
- parte, dir_adjuntos
+ parte, dir_adjuntos, dir_adjuntos_cronologia
)
mensajes.extend(mensajes_adjuntos)
else:
- ruta_adjunto = guardar_adjunto(parte, dir_adjuntos)
- if ruta_adjunto:
- adjuntos.append(Path(ruta_adjunto).name)
+ # Imagen adjunta (no inline): solo guardar en adjuntos
+ if content_type.startswith("image/"):
+ 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:
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":
html_content = _get_payload_safely(mensaje)
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:
subject = part_subject
else:
@@ -386,7 +483,7 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
subject=subject,
adjuntos=adjuntos,
)
- print(f" ✉️ Mensaje extraído:")
+ print(" ✉️ Mensaje extraído:")
print(f" - Subject: {subject}")
print(f" - Remitente: {remitente}")
print(f" - Fecha: {fecha}")
@@ -395,7 +492,7 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
print(f" - Hash generado: {mensaje_nuevo.hash}")
mensajes.append(mensaje_nuevo)
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:
print(f"Error procesando mensaje: {str(e)}")
@@ -407,7 +504,7 @@ def _parsear_fecha(fecha_str):
try:
fecha = parsedate_to_datetime(fecha_str)
return fecha.replace(tzinfo=None) # Remove timezone info
- except:
+ except Exception:
try:
fecha_match = re.search(
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)
return datetime(int(año), mes_num, int(dia), int(hora), int(minuto))
- except:
+ except Exception:
pass
return datetime.now()
diff --git a/backend/script_groups/EmailCrono/x1.py b/backend/script_groups/EmailCrono/x1.py
index f1c7f63..d76452c 100644
--- a/backend/script_groups/EmailCrono/x1.py
+++ b/backend/script_groups/EmailCrono/x1.py
@@ -62,6 +62,7 @@ def main():
# Construir rutas de salida en working_directory
output_file = os.path.join(working_directory, cronologia_file)
attachments_path = os.path.join(working_directory, attachments_dir)
+ attachments_crono_path = os.path.join(attachments_path, "cronologia")
# Debug prints
print(f"Working/Output directory: {working_directory}")
@@ -80,6 +81,7 @@ def main():
# Asegurar directorios de salida
os.makedirs(working_directory, 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
input_path = Path(input_dir)
@@ -110,7 +112,9 @@ def main():
print(f"\n{'='*60}")
print(f"Processing file: {archivo}")
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}")
sys.stdout.flush()
total_procesados += len(nuevos_mensajes)
diff --git a/backend/script_groups/OllamaTools/templates/index.html b/backend/script_groups/OllamaTools/templates/index.html
index 610f9f0..47db08b 100644
--- a/backend/script_groups/OllamaTools/templates/index.html
+++ b/backend/script_groups/OllamaTools/templates/index.html
@@ -135,6 +135,21 @@
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"] {
padding: 12px 16px;
border: 2px solid #e2e8f0;
@@ -461,6 +476,18 @@
+