primer commit

This commit is contained in:
george committed 2026-06-01 16:31:24 -05:00
commit 0855948192
11 files changed
+617

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
__pycache__/
*.pyc
.env
venv/
.DS_Store
.vscode/
.idea/
logs/
adjuntos/
+24
View File
@@ -0,0 +1,24 @@
{
"hana": {
"server": "192.168.9.13",
"port": 30015,
"user": "SYSTEM",
"password": ""
},
"outlook": {
"client_id": "671f6154-715f-4e52-aef9-94ad108dc1d4",
"client_secret": "oG58Q~yufghPKmZuh0oVqX6rcLedLP1HTwIC-bq5",
"tenant_id": "5ac66bf8-fbbd-4615-80c0-8e3d521dc5e6",
"download_folder": "./adjuntos",
"verify_ssl": false,
"ca_bundle": null
},
"client": {
"email": "contabilidad1@aberio.com.pe",
"ruta_descarga": "./adjuntos",
"ruta_final": "./resultado",
"base_datos": "SBO_ABERIO_PRODUCCION",
"usuario": "manager",
"password": "s1st3m4s"
}
}
+22
View File
@@ -0,0 +1,22 @@
import json
from pathlib import Path
from config_model import Config, HanaConfig, OutlookConfig, ClientConfig
########################################################
CONFIG_PATH = Path(__file__).resolve().parent / "config.json"
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
data = json.load(f)
# Instantiate dataclasses
hana = HanaConfig(**data.get('hana', {}))
outlook = OutlookConfig(**data.get('outlook', {}))
client = ClientConfig(**data.get('client', {}))
CONFIG = Config(
hana=hana,
outlook=outlook,
client=client
)
########################################################
+33
View File
@@ -0,0 +1,33 @@
from dataclasses import dataclass
from typing import Optional
@dataclass
class HanaConfig:
server: str
port: int
user: str
password: str
@dataclass
class OutlookConfig:
client_id: str
client_secret: str
tenant_id: str
download_folder: str
verify_ssl: bool
ca_bundle: Optional[str]
@dataclass
class ClientConfig:
email: str
ruta_descarga: str
ruta_final: str
base_datos: str
usuario: str
password: str
@dataclass
class Config:
hana: HanaConfig
outlook: OutlookConfig
client: ClientConfig
+3
View File
@@ -0,0 +1,3 @@
def procesar_correo():
pass
+111
View File
@@ -0,0 +1,111 @@
import sqlite3
DB_NAME = "emails.db"
########################################################
def init_db():
"""
Inicializa la base de datos SQLite y crea la tabla de correos si no existe.
"""
conn = sqlite3.connect(DB_NAME)
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
)
''')
conn.commit()
conn.close()
########################################################
def save_emails(emails_list):
"""
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)
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()
rows_affected = cursor.rowcount
conn.close()
return rows_affected
########################################################
def get_existing_ids(ids_list):
"""
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)
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()])
conn.close()
return existing_ids
########################################################
def delete_email_by_id(email_id):
"""
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)
cursor = conn.cursor()
cursor.execute("DELETE FROM emails WHERE id = ?", (email_id,))
conn.commit()
rows_affected = cursor.rowcount
conn.close()
return rows_affected > 0
########################################################
+78
View File
@@ -0,0 +1,78 @@
import os
import zipfile
import database_utilities
########################################################
def quitar_correos_ya_registrados_en_base_de_datos(emails):
"""
Verifica cuáles correos son nuevos (no están en la BD), los guarda
y retorna la lista filtrada de los que tienen adjuntos.
"""
if not emails:
return []
# 1. Verificar cuáles ya existen en la BD
ids = [e['id'] for e in emails]
existentes = database_utilities.get_existing_ids(ids)
# Solo nos quedamos con los que NO están en la BD
correos_no_registrados = [e for e in emails if e['id'] not in existentes]
print(f"Correos nuevos detectados: {len(correos_no_registrados)}")
if correos_no_registrados:
print("Guardando nuevos correos en la base de datos...")
database_utilities.save_emails(correos_no_registrados)
return correos_no_registrados
########################################################
def descomprimir_adjuntos(emails, ruta):
"""
Esta funcion recibe la lista de emails y por cada no recorre sus adjuntos
si encuentra un adjunto .zip lo descomprime en la ruta especificada
y actualiza la lista de archivos agregar estos nuevos documentos al lsitado de adjuntos del email
"""
archivos_finales = []
for archivo in archivos:
ruta_archivo = os.path.join(ruta, archivo)
if archivo.lower().endswith('.zip') and os.path.exists(ruta_archivo):
try:
with zipfile.ZipFile(ruta_archivo, 'r') as zip_ref:
nombres_extraidos = zip_ref.namelist()
zip_ref.extractall(ruta)
archivos_finales.extend(nombres_extraidos)
except Exception as e:
print(f" ⚠️ Error descomprimiendo {archivo}: {e}")
archivos_finales.append(archivo)
else:
archivos_finales.append(archivo)
return archivos_finales
########################################################
def filtrar_archivos_validos(archivos):
"""
Retorna una lista solo con archivos .pdf y .xml (ignora mayúsculas/minúsculas).
"""
extensiones_validas = ('.pdf', '.xml')
return [a for a in archivos if a.lower().endswith(extensiones_validas)]
########################################################
def procesar_adjuntos_email(e, archivos, ruta_descarga):
"""
Descomprime y filtra los archivos de un correo.
Retorna la lista de archivos válidos si existen, de lo contrario lista vacía.
"""
# 1. Descomprimir archivos ZIP si existen
archivos_procesados = descomprimir_adjuntos(archivos, ruta_descarga)
# 2. Filtrar solo PDF y XML
archivos_validos = filtrar_archivos_validos(archivos_procesados)
if archivos_validos:
e['archivos'] = archivos_validos
return archivos_validos
return []
########################################################
BIN
View File
Binary file not shown.
+70
View File
@@ -0,0 +1,70 @@
import sys
import os
from datetime import datetime, timedelta
########################################################
class MultiStream:
"""
Clase para redirigir la salida a múltiples flujos (consola y archivo).
"""
def __init__(self, *streams):
self.streams = streams
def write(self, data):
for stream in self.streams:
stream.write(data)
stream.flush()
def flush(self):
for stream in self.streams:
stream.flush()
########################################################
def setup_logger():
"""
Configura la redirección de stdout y stderr a un archivo de log y a la consola.
Retorna la ruta del archivo de log creado.
"""
log_dir = "logs"
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# Nombre del archivo: YYYY-MM-DD_HH-MM-SS.log
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
log_filename = f"{timestamp}.log"
log_path = os.path.join(log_dir, log_filename)
# Abrir archivo en modo append con encoding utf-8 para soportar iconos
log_file = open(log_path, "a", encoding="utf-8")
# Redirigir stdout y stderr
sys.stdout = MultiStream(sys.stdout, log_file)
sys.stderr = MultiStream(sys.stderr, log_file)
return log_path
########################################################
def limpiar_logs_antiguos(dias=7):
"""
Elimina archivos de log con más de 'n' días de antigüedad.
"""
log_dir = "logs"
if not os.path.exists(log_dir):
return
limite = datetime.now() - timedelta(days=dias)
count = 0
for archivo in os.listdir(log_dir):
ruta_archivo = os.path.join(log_dir, archivo)
if os.path.isfile(ruta_archivo) and archivo.endswith(".log"):
fecha_archivo = datetime.fromtimestamp(os.path.getmtime(ruta_archivo))
if fecha_archivo < limite:
try:
os.remove(ruta_archivo)
count += 1
except Exception as e:
print(f"⚠️ Error al eliminar log {archivo}: {e}")
if count > 0:
print(f"🧹 Se eliminaron {count} archivos de log antiguos (más de {dias} días).")
+127
View File
@@ -0,0 +1,127 @@
import sys
import json
import os
import email_utilities
import outlook_utilities
import database_utilities
import logger_utilities
from datetime import datetime, timedelta
from config_loader import CONFIG
########################################################
def obtener_token():
try:
print("Intentando obtener token de acceso...")
outlook_token = outlook_utilities.get_access_token()
print("✅ Token obtenido exitosamente.")
print(f"Token prefix: {outlook_token[:25]}...")
return outlook_token
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
########################################################
def descargar_correos(outlook_token, fecha_inicio, fecha_fin):
try:
print("\n" + "="*60)
print(f"[MAIL] {CONFIG.client.email}")
emails = outlook_utilities.get_emails(CONFIG.client.email, outlook_token, fecha_inicio, fecha_fin)
print(f"Se encontraron {len(emails)} correos.")
return emails
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
return []
########################################################
def descargar_adjuntos_de_correos(outlook_token, emails, ruta_descarga):
try:
for email in emails:
# Guardar lista original para el log
adjuntos = outlook_utilities.download_attachments(
CONFIG.client.email,
email['id'],
outlook_token,
ruta_descarga)
if adjuntos:
email['adjuntos'] = adjuntos
return emails
except Exception as ex:
print(f" ❌ Error procesando {email['id'][:10]}: {ex}")
return []
########################################################
def main():
# Inicializar Base de Datos
database_utilities.init_db()
# Calcular fechas
hoy = datetime.now()
ayer = hoy - timedelta(days=25)
fecha_inicio = ayer.strftime("%Y-%m-%d")
fecha_fin = hoy.strftime("%Y-%m-%d")
print(f"Rango de búsqueda: {fecha_inicio} al {fecha_fin}")
outlook_token = obtener_token()
# se descarga todos los correos en el rango de fecha ingresado
# tengan o no adjuntos
emails = descargar_correos(outlook_token, fecha_inicio, fecha_fin)
# se verifica si estos correos ya estan en la base de datos
# si ya esta registrado, se quita del listado
# si no esta se inserta en la base de datos
emails = email_utilities.quitar_correos_ya_registrados_en_base_de_datos(emails)
# descargar todos los adjuntos de cada mail
# si el correo tiene archivos zip, se descomprimen y se agregan a
# la lista de archivos adjuntos
# se evalua los adjuntos del correo:
# 1. se quita de la lista el correo si no tiene adjunto
# 2. se quita de la lista el correo si no tiene
for e in emails:
print(f"\n📬 [{e['fecha']}] {e['subject'][:80]}...")
archivos_adjuntos = descargar_adjunto_de_correo(outlook_token, e['id'], CONFIG.client.ruta_descarga)
########################################################
if __name__ == "__main__":
# Configurar Logger (Redirige stdout a archivo y consola)
log_path = logger_utilities.setup_logger()
logger_utilities.limpiar_logs_antiguos(dias=30)
inicio_ejecucion = datetime.now()
print("="*60)
print(f"🚀 INICIO DE EJECUCIÓN: {inicio_ejecucion.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"📄 Log guardado en: {log_path}")
print("="*60)
main()
fin_ejecucion = datetime.now()
duracion = fin_ejecucion - inicio_ejecucion
print("\n" + "="*60)
print(f"🏁 FIN DE EJECUCIÓN: {fin_ejecucion.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"⏱️ Duración total: {str(duracion).split('.')[0]}") # Sin microsegundos
print("="*60)
+140
View File
@@ -0,0 +1,140 @@
import msal
import requests
import warnings
import base64
import os
from datetime import datetime, timedelta
from config_loader import CONFIG
warnings.filterwarnings("ignore")
########################################################
def get_access_token():
"""
Obtiene un token de acceso de Microsoft Graph usando las credenciales
configuradas en config.json (Client ID, Client Secret y Tenant ID).
"""
if not CONFIG or not CONFIG.outlook:
raise ValueError("La configuración de Outlook no está cargada correctamente.")
authority = f"https://login.microsoftonline.com/{CONFIG.outlook.tenant_id}"
app = msal.ConfidentialClientApplication(
CONFIG.outlook.client_id,
authority=authority,
client_credential=CONFIG.outlook.client_secret,
verify=CONFIG.outlook.verify_ssl
)
scopes = ["https://graph.microsoft.com/.default"]
result = app.acquire_token_for_client(scopes=scopes)
if "access_token" in result:
return result["access_token"]
else:
error = result.get("error")
error_description = result.get("error_description")
correlation_id = result.get("correlation_id")
raise Exception(f"Error al obtener token: {error} - {error_description} (ID: {correlation_id})")
########################################################
def get_emails(user_email, token, fecha_inicio, fecha_fin):
"""
Obtiene los correos de una cuenta específica en un rango de fechas.
Retorna una lista de JSON con: id, fecha (ajustada -5h), sender, subject y has_attach.
"""
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
dt_inicio_utc = datetime.strptime(fecha_inicio, "%Y-%m-%d") + timedelta(hours=5)
dt_fin_utc = datetime.strptime(fecha_fin, "%Y-%m-%d") + timedelta(days=1, hours=5)
filter_query = (
f"receivedDateTime ge {dt_inicio_utc.strftime('%Y-%m-%dT%H:%M:%SZ')} and "
f"receivedDateTime lt {dt_fin_utc.strftime('%Y-%m-%dT%H:%M:%SZ')}"
)
params = {
"$filter": filter_query,
"$select": "id,receivedDateTime,hasAttachments,subject,from",
"$top": 50
}
url = f"https://graph.microsoft.com/v1.0/users/{user_email}/messages"
all_mails = []
while url:
response = requests.get(
url,
headers=headers,
params=params,
verify=CONFIG.outlook.verify_ssl
)
if response.status_code != 200:
raise Exception(f"Error Graph API ({user_email}): {response.status_code} - {response.text}")
data = response.json()
messages = data.get("value", [])
for msg in messages:
utc_str = msg.get("receivedDateTime")
dt_utc = datetime.fromisoformat(utc_str.replace("Z", "+00:00"))
dt_local = dt_utc - timedelta(hours=5)
from_obj = msg.get("from", {})
sender = from_obj.get("emailAddress", {}).get("address", "Desconocido")
all_mails.append({
"id": msg.get("id"),
"fecha": dt_local.strftime("%Y-%m-%d %H:%M:%S"),
"sender": sender,
"subject": msg.get("subject", ""),
"has_attach": msg.get("hasAttachments")
})
url = data.get("@odata.nextLink")
params = None
return all_mails
########################################################
def download_attachments(user_email, message_id, token, download_path):
"""
Descarga los adjuntos de un mensaje específico y los guarda en la ruta indicada.
Retorna una lista con los nombres de los archivos descargados.
"""
if not os.path.exists(download_path):
os.makedirs(download_path, exist_ok=True)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
url = f"https://graph.microsoft.com/v1.0/users/{user_email}/messages/{message_id}/attachments"
response = requests.get(url, headers=headers, verify=CONFIG.outlook.verify_ssl)
if response.status_code != 200:
raise Exception(f"Error al obtener adjuntos ({message_id}): {response.status_code} - {response.text}")
attachments = response.json().get("value", [])
downloaded_files = []
for attachment in attachments:
if attachment.get("@odata.type") == "#microsoft.graph.fileAttachment":
file_name = attachment.get("name")
content_bytes = attachment.get("contentBytes")
if content_bytes:
file_path = os.path.join(download_path, file_name)
with open(file_path, "wb") as f:
f.write(base64.b64decode(content_bytes))
downloaded_files.append(file_name)
return downloaded_files
########################################################