---
name: imap_search
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def imap_search(conn, criteria: str = 'UNSEEN', mailbox: str = '') -> dict"
description: "Busca mensajes en un buzon IMAP por criterio y devuelve sus UIDs. Sobre una conexion imaplib viva (de imap_connect), opcionalmente hace select(mailbox) y luego conn.uid('SEARCH', None, criteria). Usa SIEMPRE UIDs (estables mientras no cambie UIDVALIDITY), no numeros de secuencia (que se renumeran al borrar). criteria es una expresion IMAP cruda RFC 3501 (UNSEEN, ALL, FROM x, SUBJECT y, SINCE 01-Jan-2026, combinaciones). Devuelve {status:'ok', uids:[int], count} o {status:'error', error}. Nunca lanza."
tags: [email, imap, infra, search, network]
params:
  - name: conn
    desc: "Objeto imaplib.IMAP4[_SSL] vivo y autenticado, producido por imap_connect. None devuelve status error."
  - name: criteria
    desc: "Expresion de busqueda IMAP cruda (RFC 3501 SEARCH). Ej: 'UNSEEN', 'ALL', 'FROM foo@bar.com', 'SUBJECT factura', 'SINCE 01-Jan-2026', 'UNSEEN SINCE 01-Jun-2026'. Vacio devuelve status error. Default 'UNSEEN'."
  - name: mailbox
    desc: "Si no esta vacio, se hace select(mailbox) antes de buscar (ej. '[Gmail]/Sent Mail'). Vacio (default) usa el buzon ya seleccionado."
output: "dict de estado. En exito {status:'ok', uids: list[int], count: int}: uids son UIDs (no numeros de secuencia), ordenados como los devuelve el servidor; lista vacia si nada casa (sigue siendo status ok). En fallo (conn None, criteria vacio o mal formado, buzon inexistente) {status:'error', error: str}."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_py_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/infra/imap_search.py"
---

## Ejemplo

```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from infra import imap_connect, imap_search, imap_fetch_message

c = imap_connect("imap.gmail.com", 993, "gutierenmanuel15@gmail.com", "abcd efgh ijkl mnop")
conn = c["conn"]

# No leidos del INBOX (buzon ya seleccionado por imap_connect)
res = imap_search(conn, criteria="UNSEEN")
print(res["status"], res["count"])   # "ok" 7
print(res["uids"])                   # [1422, 1425, 1431, ...]

# Buscar en otra carpeta sin reconectar
sent = imap_search(conn, criteria="SINCE 01-Jun-2026", mailbox="[Gmail]/Sent Mail")

# Fetch del primer UID encontrado
if res["uids"]:
    msg = imap_fetch_message(conn, res["uids"][0])
    print(msg["message"]["subject"])

conn.logout()
```

## Cuando usarla

Usala tras `imap_connect` cuando necesites localizar mensajes por criterio
(no leidos, de un remitente, por asunto, por fecha) antes de leerlos con
`imap_fetch_message`. Es el paso intermedio del flujo lectura: connect -> search
-> fetch. Para barrer una carpeta distinta del INBOX pasa `mailbox` y evita
una reconexion.

## Gotchas

- Funcion impura: hace red sobre el `conn` vivo. Nunca lanza: comprueba `status`.
  El `conn` lo provee `imap_connect` (no resuelve credenciales).
- Devuelve UIDs, NO numeros de secuencia. Importante: guarda y reutiliza los
  UIDs; los seq cambian cuando se borran mensajes, los UIDs no (salvo cambio de
  UIDVALIDITY del buzon, raro). `imap_fetch_message` tambien espera UID.
- El `criteria` es sintaxis IMAP cruda, sin validar: un criterio mal formado
  hace que el servidor responda no-OK y devuelve `status:'error'`. Las fechas
  van en formato IMAP `DD-Mon-YYYY` (ej. `01-Jan-2026`), no ISO.
- `SEARCH` por defecto opera sobre US-ASCII; para acentos en `SUBJECT`/`FROM`
  algunos servidores requieren `CHARSET UTF-8` (este wrapper pasa `None` como
  charset, que cubre el caso comun). Si necesitas charset, busca por cabeceras
  ASCII o filtra el resultado en cliente.
- Una busqueda sin coincidencias devuelve `status:'ok'` con `uids:[]` y
  `count:0` — distingue "sin resultados" mirando `count`, no `status`.
- Cierra con `conn.logout()` al terminar (responsabilidad del caller).
