poplib en Python: lee correos con POP3

Publicado el: 22/08/2026
Tempo de leitura: 6 minutos
A retro blue mailbox attached to a vibrant yellow wall, perfect for vintage themes.

El módulo poplib implementa un cliente POP3 en la biblioteca estándar de Python. Permite consultar un buzón, listar mensajes, descargar contenido y marcar elementos para borrado. POP3 todavía aparece en cuentas antiguas, dispositivos e integraciones sencillas que retiran correos de un servidor.

La propia documentación de Python considera POP3 obsoleto y recomienda IMAP cuando está disponible. POP3 suele ofrecer una vista simple del buzón sin carpetas, búsquedas ricas, sincronización de flags ni acceso parcial confiable. Úsalo únicamente cuando el proveedor o sistema heredado lo exija.

Conexión segura con POP3_SSL

import poplib
import ssl

contexto = ssl.create_default_context()

cliente = poplib.POP3_SSL(
    "pop.example.com",
    port=995,
    timeout=15,
    context=contexto,
)

try:
    cliente.user("usuario@example.com")
    cliente.pass_("contraseña")
    print(cliente.stat())
finally:
    cliente.quit()

Usa POP3_SSL o STARTTLS antes de autenticar. POP3 simple puede exponer usuario, contraseña y contenido de mensajes. El contexto creado con ssl.create_default_context() verifica certificado y hostname.

No desactives la verificación para corregir certificados caducados o nombres incorrectos. Consulta ssl en Python.

STARTTLS en el puerto 110

Cuando un servidor usa upgrade TLS explícito, conecta sin autenticar, revisa capabilities y llama stls() con un contexto verificado.

cliente = poplib.POP3("pop.example.com", timeout=15)
cliente.stls(context=contexto)
cliente.user(USER)
cliente.pass_(PASSWORD)

STARTTLS debe ocurrir antes de USER y PASS. Rechaza la conexión si falta la capability esperada; nunca hagas downgrade silencioso a texto claro.

Credenciales

No fijes contraseñas en el código. Usa variables de entorno, un gestor de secretos o credenciales de aplicación emitidas por el proveedor. Muchos servicios modernos exigen OAuth y pueden rechazar contraseñas POP3 normales.

No actives set_debuglevel(2) en producción. El debug del protocolo puede revelar metadatos, identificadores, respuestas y detalles de autenticación.

Consultar capabilities

capacidades = cliente.capa()
for nombre, parametros in capacidades.items():
    print(nombre, parametros)

Las capabilities pueden anunciar STLS, UIDL, TOP, UTF8 y mecanismos de autenticación. Las implementaciones POP3 varían mucho. Que un método exista en Python no garantiza que el servidor lo soporte correctamente.

Estado del buzón

cantidad, bytes_totales = cliente.stat()
print(cantidad, bytes_totales)

stat() devuelve cantidad y tamaño total informado. Trátalos como estimaciones y aplica límites propios. El buzón puede cambiar entre la consulta y la descarga.

Listar mensajes

respuesta, lineas, octetos = cliente.list()

mensajes = []
for linea in lineas:
    numero_texto, tamano_texto = linea.split(maxsplit=1)
    mensajes.append((int(numero_texto), int(tamano_texto)))

Valida cada línea porque el servidor remoto puede enviar datos inesperados. Ignora mensajes superiores al límite de la aplicación y restringe cuántos se procesan por ejecución.

Usa UIDL para identificar mensajes

Los números POP3 son posiciones temporales de la sesión. Usa uidl() para obtener identificadores del servidor y evitar procesar el mismo mensaje repetidamente.

respuesta, lineas, octetos = cliente.uidl()

uids = {}
for linea in lineas:
    numero, uid = linea.split(maxsplit=1)
    uids[int(numero)] = uid.decode("ascii", errors="strict")

Guarda los UIDs completados en base de datos o archivo transaccional. El identificador depende del servidor y no sustituye Message-ID; ambos pueden ayudar a deduplicar.

Descargar un mensaje completo

respuesta, lineas, octetos = cliente.retr(numero)

if octetos > LIMITE_MENSAJE:
    raise ValueError("mensaje demasiado grande")

raw_email = b"\r\n".join(lineas) + b"\r\n"

retr() devuelve líneas sin el terminador final. Reconstruye correctamente el stream de bytes. La respuesta puede ocupar memoria, por lo que conviene inspeccionar primero el tamaño de LIST y limitar bytes totales por ejecución.

Parsear con el paquete email

from email import policy
from email.parser import BytesParser

mensaje = BytesParser(policy=policy.default).parsebytes(raw_email)

print(mensaje.get("Subject"))
print(mensaje.get("From"))

Asuntos, remitentes, cuerpos HTML y nombres de adjuntos son datos no confiables. Escapa headers al mostrarlos y sanitiza HTML si realmente lo renderizas.

TOP para headers y vista previa

top(numero, lineas) intenta descargar headers y algunas líneas del cuerpo. La documentación advierte que TOP está mal especificado y suele estar roto en servidores alternativos.

respuesta, lineas, octetos = cliente.top(numero, 0)
headers = b"\r\n".join(lineas) + b"\r\n\r\n"

Prueba TOP manualmente con cada proveedor antes de depender de él. Cuando sea inconsistente, usa RETR con límites o elige IMAP.

Extraer texto con límites

def partes_texto(mensaje, limite=1_000_000):
    total = 0
    for parte in mensaje.walk():
        if parte.get_content_maintype() == "multipart":
            continue
        if parte.get_content_disposition() == "attachment":
            continue
        datos = parte.get_payload(decode=True) or b""
        total += len(datos)
        if total > limite:
            raise ValueError("el contenido supera el límite")
        yield parte.get_content_type(), datos

Decodifica con el charset declarado y fallback seguro. Consulta codecs en Python.

Adjuntos seguros

Nunca uses un filename MIME como ruta directa. Genera un identificador interno, aplica límites por archivo y totales, inspecciona el formato real y almacena fuera de directorios ejecutables.

Usa tempfile en Python y hashlib en Python.

El borrado se confirma con QUIT

dele(numero) marca un mensaje durante la sesión. En servidores conformes, el borrado se hace permanente cuando quit() termina correctamente.

cliente.dele(numero)
# otras comprobaciones
cliente.quit()  # confirma cambios

Esto introduce riesgo: el código puede marcar mensajes incorrectos y confirmar todo al cerrar. Separa descarga de borrado, confirma UID y estado local y registra una decisión durable antes de llamar DELE.

Deshacer marcas con RSET

cliente.rset()

rset() elimina marcas de borrado de la sesión antes de confirmarlas. Úsalo cuando falle el procesamiento tras uno o varios DELE.

Desconexiones inesperadas

La mayoría de servidores cancela borrados pendientes si la conexión termina sin QUIT, pero la documentación menciona implementaciones históricas que incumplen esa regla. No uses el disconnect como rollback garantizado. Retrasa DELE hasta la fase final.

Pipeline de procesamiento seguro

Un flujo robusto puede seguir estos pasos:

  1. Conectar con TLS verificado.
  2. Listar UIDL y tamaños.
  3. Ignorar UIDs ya completados.
  4. Descargar como máximo N mensajes y M bytes.
  5. Parsear y validar.
  6. Persistir resultado y confirmar estado local.
  7. Solo entonces marcar opcionalmente para borrado.
  8. Ejecutar QUIT y registrar finalización.

Si falla la persistencia local, deja el mensaje intacto.

Modo UTF-8

utf8() solicita el modo RFC 6856 cuando el servidor lo soporta. Revisa capabilities y trata error_proto. Incluso con UTF-8 en POP3, las partes MIME pueden declarar muchos charsets.

Keep-alive y deadlines

noop() puede mantener activa la sesión, pero no sustituye un plazo total. El procesamiento largo debe limitar duración y reconectar de forma segura.

Tratamiento de errores

poplib.error_proto representa respuestas POP3. Los fallos de socket y TLS pueden llegar como OSError, TimeoutError o ssl.SSLError.

try:
    cliente = poplib.POP3_SSL(HOST, context=contexto, timeout=15)
    cliente.user(USER)
    cliente.pass_(PASSWORD)
except poplib.error_proto as error:
    raise RuntimeError("el servidor POP3 rechazó la operación") from error
except OSError as error:
    raise RuntimeError("fallo de transporte POP3") from error

Nunca incluyas contraseña ni respuesta completa de autenticación en logs.

POP3 frente a IMAP

POP3 se centra en descargar desde un buzón simple. IMAP ofrece carpetas, búsqueda en servidor, UIDs con UIDVALIDITY, flags y sincronización. Para flujos que preservan estado, consulta imaplib en Python.

Pruebas recomendadas

Prueba certificado inválido, STARTTLS ausente, login rechazado, UIDL no soportado, TOP roto, mensaje grande, MIME malformado, adjunto peligroso, DELE seguido de RSET, fallo antes de QUIT, timeout, buzón vacío y deduplicación.

Errores comunes

Los fallos frecuentes son usar POP3 simple con contraseña, desactivar TLS, confiar en TOP, identificar solo por número, descargar todo el buzón, guardar todo en memoria, confiar en filenames, marcar borrado antes de persistir, confirmar DELE automáticamente al cerrar y registrar contenido sensible.

Conclusión

poplib ofrece acceso directo a POP3, pero el protocolo es limitado y antiguo. Usa POP3_SSL o STARTTLS con contexto verificado, identifica mediante UIDL, impone límites, parsea MIME como contenido no confiable y convierte el borrado en una acción final explícitamente autorizada.

Consulta la documentación oficial de poplib y el RFC 1939 de POP3. Cuando sea posible, prefiere IMAP para sincronización y control más predecibles.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Código Python que representa functools.Placeholder y argumentos posicionales
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    functools.Placeholder: argumentos flexibles en partial

    Aprende functools.Placeholder en Python 3.14 para crear llamadas parciales flexibles con argumentos posicionales abiertos.

    Ler mais

    Tempo de leitura: 6 minutos
    17/10/2026
    Código en una pantalla que representa t-strings en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    T-strings en Python 3.14: templates estructurados

    Aprende t-strings en Python 3.14, explora Template e Interpolation y crea renderizadores estructurados, extensibles y seguros.

    Ler mais

    Tempo de leitura: 6 minutos
    16/10/2026
    CPU y módulos de memoria que representan shared_memory track en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    shared_memory track: controla el resource tracker

    Aprende shared_memory track en Python para controlar el resource tracker, la propiedad y la limpieza entre procesos.

    Ler mais

    Tempo de leitura: 5 minutos
    16/10/2026
    Unidad de almacenamiento de datos que representa compression.zstd en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    compression.zstd: streaming y compresión segura

    Aprende compression.zstd en Python para comprimir archivos y datos con velocidad, streaming, diccionarios y límites seguros.

    Ler mais

    Tempo de leitura: 7 minutos
    15/10/2026
    Código en pantalla para diagnosticar procesos con sys.remote_exec en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    sys.remote_exec: diagnostica procesos Python

    Aprende sys.remote_exec en Python para diagnosticar procesos activos con scripts controlados, permisos seguros y buenas prácticas.

    Ler mais

    Tempo de leitura: 7 minutos
    15/10/2026
    Código Python y configuración de variables de entorno con os.reload_environ
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    os.reload_environ: threads y variables de entorno

    Aprende os.reload_environ en Python para actualizar variables de entorno externas con validación, compatibilidad y precauciones de concurrencia.

    Ler mais

    Tempo de leitura: 5 minutos
    14/10/2026