Files
imap_migrate/config.py
T
2026-09-10 09:24:22 -06:00

160 lines
6.6 KiB
Python

"""Parseo de argumentos de línea de comandos y configuración de la aplicación."""
from __future__ import annotations
import argparse
import getpass
import os
import sys
from dataclasses import dataclass
DEFAULT_PORT = 993
DEFAULT_TIMEOUT = 30
DEFAULT_RETRIES = 3
DEFAULT_STATE_FILE = "migration-state.json"
DEFAULT_LOG_DIR = "logs"
DEFAULT_HEADER_BATCH_SIZE = 500
EXIT_PARAM_ERROR = 2
@dataclass
class ServerConfig:
label: str
host: str
port: int
user: str
password: str
domain: str | None = None
ip: str | None = None
verify_tls: bool = True
use_ssl: bool = True
@property
def connect_host(self) -> str:
return self.ip or self.host
@property
def tls_hostname(self) -> str:
return self.host
@dataclass
class AppConfig:
server_a: ServerConfig
server_b: ServerConfig
timeout: int
retries: int
state_file: str
log_dir: str
header_batch_size: int
verbose: bool
def _resolve_password(cli_value: str | None, env_var: str, prompt_label: str) -> str:
if cli_value:
return cli_value
env_value = os.environ.get(env_var)
if env_value:
return env_value
try:
return getpass.getpass(f"{prompt_label}: ")
except (EOFError, KeyboardInterrupt):
print("\nNo se proporcionó contraseña.", file=sys.stderr)
sys.exit(EXIT_PARAM_ERROR)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="imap_migrate.py",
description="Migra o respalda el contenido completo de una cuenta IMAP entre dos servidores.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Ejemplo:
python3 imap_migrate.py \\
--aserver imap.servidor-a.com --aport 993 \\
--auser usuario@dominio.com --apassword 'passwordA' \\
--bserver imap.servidor-b.com --bport 993 \\
--buser usuario@dominio.com --bpassword 'passwordB'
Si no se proporcionan las contraseñas, se solicitarán de forma interactiva
(o pueden definirse en las variables de entorno IMAP_A_PASSWORD / IMAP_B_PASSWORD).
Ejecutar el programa varias veces sobre el mismo --state-file continúa una
migración incremental sin volver a copiar mensajes ya migrados.
""",
)
group_a = parser.add_argument_group("Servidor origen (A)")
group_a.add_argument("--aserver", required=True, help="Host del servidor IMAP origen")
group_a.add_argument("--aport", type=int, default=DEFAULT_PORT, help=f"Puerto del servidor origen (por defecto {DEFAULT_PORT})")
group_a.add_argument("--auser", required=True, help="Usuario/cuenta en el servidor origen")
group_a.add_argument("--apassword", default=None, help="Password del servidor origen (si se omite, se solicita de forma interactiva)")
group_a.add_argument("--adomain", default=None, help="Dominio lógico de la cuenta origen (informativo / uso futuro multi-cuenta)")
group_a.add_argument("--aip", default=None, help="IP a la que conectar en el servidor origen, usando --aserver como hostname TLS")
group_a.add_argument("--a-no-verify-tls", action="store_true", help="Deshabilita verificación de certificado TLS en el servidor origen (inseguro)")
group_a.add_argument("--a-no-ssl", action="store_true", help="Conecta sin SSL/TLS al servidor origen (inseguro, no recomendado)")
group_b = parser.add_argument_group("Servidor destino (B)")
group_b.add_argument("--bserver", required=True, help="Host del servidor IMAP destino")
group_b.add_argument("--bport", type=int, default=DEFAULT_PORT, help=f"Puerto del servidor destino (por defecto {DEFAULT_PORT})")
group_b.add_argument("--buser", required=True, help="Usuario/cuenta en el servidor destino")
group_b.add_argument("--bpassword", default=None, help="Password del servidor destino (si se omite, se solicita de forma interactiva)")
group_b.add_argument("--bdomain", default=None, help="Dominio lógico de la cuenta destino (informativo / uso futuro multi-cuenta)")
group_b.add_argument("--bip", default=None, help="IP a la que conectar en el servidor destino, usando --bserver como hostname TLS")
group_b.add_argument("--b-no-verify-tls", action="store_true", help="Deshabilita verificación de certificado TLS en el servidor destino (inseguro)")
group_b.add_argument("--b-no-ssl", action="store_true", help="Conecta sin SSL/TLS al servidor destino (inseguro, no recomendado)")
parser.add_argument("--no-verify-tls", action="store_true", help="Deshabilita verificación de certificado TLS en AMBOS servidores (inseguro)")
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help=f"Timeout de conexión en segundos (por defecto {DEFAULT_TIMEOUT})")
parser.add_argument("--retries", type=int, default=DEFAULT_RETRIES, help=f"Número de reintentos ante fallos de conexión (por defecto {DEFAULT_RETRIES})")
parser.add_argument("--state-file", default=DEFAULT_STATE_FILE, help=f"Archivo de estado de la migración (por defecto {DEFAULT_STATE_FILE})")
parser.add_argument("--log-dir", default=DEFAULT_LOG_DIR, help=f"Directorio de logs (por defecto {DEFAULT_LOG_DIR})")
parser.add_argument("--header-batch-size", type=int, default=DEFAULT_HEADER_BATCH_SIZE, help="Cantidad de mensajes a analizar por lote (por defecto %(default)s)")
parser.add_argument("--verbose", action="store_true", help="Muestra en consola los mismos detalles que se registran en el log")
return parser
def parse_args(argv: list[str] | None = None) -> AppConfig:
parser = build_parser()
args = parser.parse_args(argv)
a_password = _resolve_password(args.apassword, "IMAP_A_PASSWORD", "Password servidor A")
b_password = _resolve_password(args.bpassword, "IMAP_B_PASSWORD", "Password servidor B")
server_a = ServerConfig(
label="A",
host=args.aserver,
port=args.aport,
user=args.auser,
password=a_password,
domain=args.adomain,
ip=args.aip,
verify_tls=not (args.no_verify_tls or args.a_no_verify_tls),
use_ssl=not args.a_no_ssl,
)
server_b = ServerConfig(
label="B",
host=args.bserver,
port=args.bport,
user=args.buser,
password=b_password,
domain=args.bdomain,
ip=args.bip,
verify_tls=not (args.no_verify_tls or args.b_no_verify_tls),
use_ssl=not args.b_no_ssl,
)
return AppConfig(
server_a=server_a,
server_b=server_b,
timeout=args.timeout,
retries=args.retries,
state_file=args.state_file,
log_dir=args.log_dir,
header_batch_size=args.header_batch_size,
verbose=args.verbose,
)