import os import sqlite3 # ============================================================================ # CONFIGURACIÓN # ============================================================================ # Junto al código, no relativa al directorio de trabajo: el demonio (por ejemplo como servicio de # Windows) puede lanzarse desde cualquier carpeta y debe encontrar siempre la misma base. DB_NAME = os.path.join(os.path.dirname(os.path.abspath(__file__)), "emails.db") # ============================================================================ def init_db() -> None: """ Inicializa la base de datos SQLite y crea las tablas si no existen: ``emails`` (correos ya procesados) y ``comprobantes`` (códigos de seguimiento y resultado de cada comprobante, ver ``seguimiento.py``). """ conn = sqlite3.connect(DB_NAME) try: cursor = conn.cursor() # Crear tabla con id como llave primaria para evitar duplicados cursor.execute(''' CREATE TABLE IF NOT EXISTS emails ( id TEXT PRIMARY KEY, fecha TEXT, sender TEXT, subject TEXT, has_attach INTEGER, fecha_descarga TEXT ) ''') # Un comprobante se identifica por (ruc_emisor, tipo, serie, numero). Los # correos sin comprobante identificable llevan serie NULL y son únicos por correo. cursor.execute(''' CREATE TABLE IF NOT EXISTS comprobantes ( id INTEGER PRIMARY KEY AUTOINCREMENT, codigo_seguimiento TEXT UNIQUE, email_id TEXT NOT NULL, ruc_emisor TEXT, tipo TEXT, serie TEXT, numero TEXT, estado TEXT NOT NULL DEFAULT 'pendiente', observaciones TEXT, doc_entry INTEGER, veces_recibido INTEGER NOT NULL DEFAULT 1, notificacion_estado TEXT NOT NULL DEFAULT 'pendiente', notificacion_error TEXT, notificacion_fecha TEXT, notificacion_destinatario TEXT, intentos INTEGER NOT NULL DEFAULT 0, creado_en TEXT NOT NULL, actualizado_en TEXT NOT NULL ) ''') # Bases creadas antes de agregar una columna: se añade sin tocar los datos. columnas = {fila[1] for fila in cursor.execute("PRAGMA table_info(comprobantes)")} for columna, definicion in ( ("notificacion_destinatario", "TEXT"), ("intentos", "INTEGER NOT NULL DEFAULT 0"), ): if columna not in columnas: cursor.execute(f"ALTER TABLE comprobantes ADD COLUMN {columna} {definicion}") cursor.execute(''' CREATE UNIQUE INDEX IF NOT EXISTS ux_comprobantes_documento ON comprobantes (ruc_emisor, tipo, serie, numero) WHERE serie IS NOT NULL ''') cursor.execute(''' CREATE UNIQUE INDEX IF NOT EXISTS ux_comprobantes_correo ON comprobantes (email_id) WHERE serie IS NULL ''') conn.commit() finally: conn.close() # ============================================================================ def save_emails(emails_list: list[dict]) -> int: """ Guarda una lista de correos en la base de datos. Retorna la cantidad de registros nuevos insertados. """ if not emails_list: return 0 conn = sqlite3.connect(DB_NAME) try: cursor = conn.cursor() from datetime import datetime fecha_ahora = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Mapear lista de diccionarios a tuplas para executemany # has_attach se guarda como 1 (True) o 0 (False) data = [ ( e['id'], e['fecha'], e['sender'], e['subject'], 1 if e['has_attach'] else 0, fecha_ahora ) for e in emails_list ] # INSERT OR IGNORE evita errores si el ID ya existe cursor.executemany(''' INSERT OR IGNORE INTO emails (id, fecha, sender, subject, has_attach, fecha_descarga) VALUES (?, ?, ?, ?, ?, ?) ''', data) conn.commit() return cursor.rowcount finally: conn.close() # ============================================================================ def get_existing_ids(ids_list: list[str]) -> list[str]: """ Recibe una lista de IDs y devuelve una lista con aquellos que ya existen en la BD. """ if not ids_list: return [] conn = sqlite3.connect(DB_NAME) try: cursor = conn.cursor() existing_ids = [] # Procesar por lotes para evitar límites de parámetros en SQLite for i in range(0, len(ids_list), 900): chunk = ids_list[i:i + 900] placeholders = ','.join(['?'] * len(chunk)) query = f"SELECT id FROM emails WHERE id IN ({placeholders})" cursor.execute(query, chunk) existing_ids.extend([row[0] for row in cursor.fetchall()]) return existing_ids finally: conn.close() # ============================================================================ def delete_email_by_id(email_id: str) -> bool: """ Elimina un correo de la base de datos por su ID. Retorna True si se eliminó el registro, False si no se encontró. """ if not email_id: return False conn = sqlite3.connect(DB_NAME) try: cursor = conn.cursor() cursor.execute("DELETE FROM emails WHERE id = ?", (email_id,)) conn.commit() return cursor.rowcount > 0 finally: conn.close() # ============================================================================