---
name: imap_fetch_message
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def imap_fetch_message(conn, uid: int, mark_seen: bool = False) -> dict"
description: "Descarga y parsea un mensaje IMAP por UID a un dict estructurado. Sobre una conexion imaplib viva (de imap_connect) ejecuta conn.uid('FETCH', uid, '(BODY.PEEK[])') si mark_seen=False (NO marca leido) o '(RFC822)' si True (marca \\Seen), parsea con email.message_from_bytes y extrae from/to/cc/subject/date/message_id (cabeceras RFC 2047 decodificadas a Unicode con decode_header), body_text (text/plain) y body_html (text/html) respetando el charset de cada parte, y attachments como lista de {filename, content_type, size_bytes} SIN bajar el binario completo. Maneja multipart y mensajes simples. Devuelve {status:'ok', message:{...}} o {status:'error', error}. Nunca lanza."
tags: [email, imap, infra, parse, network]
params:
  - name: conn
    desc: "Objeto imaplib.IMAP4[_SSL] vivo y autenticado, producido por imap_connect. None devuelve status error."
  - name: uid
    desc: "UID del mensaje (de imap_search). NO numero de secuencia. No-entero devuelve status error."
  - name: mark_seen
    desc: "False (default) usa BODY.PEEK[] y NO marca el mensaje como leido. True usa RFC822 y lo marca \\Seen."
output: "dict de estado. En exito {status:'ok', message:{uid:int, from:str, to:str, cc:str, subject:str, date:str, message_id:str, body_text:str (text/plain concatenado), body_html:str (text/html concatenado), attachments:[{filename:str, content_type:str, size_bytes:int}]}}: cabeceras decodificadas de RFC 2047 a Unicode; cuerpos decodificados respetando el charset declarado. En fallo (conn None, uid no-entero o inexistente, FETCH no OK) {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_fetch_message.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"]

# Localiza no leidos y lee el primero SIN marcarlo como leido (PEEK)
found = imap_search(conn, criteria="UNSEEN")
if found["uids"]:
    res = imap_fetch_message(conn, found["uids"][0], mark_seen=False)
    m = res["message"]
    print(m["from"])          # 'Soporte <soporte@banco.es>'
    print(m["subject"])       # 'Tu factura de junio'  (acentos ya decodificados)
    print(m["date"])
    print(m["body_text"][:200])
    for att in m["attachments"]:
        print(att["filename"], att["content_type"], att["size_bytes"])

conn.logout()
```

## Cuando usarla

Usala como ultimo paso del flujo de lectura (connect -> search -> fetch) cuando
ya tienes un UID y quieres el contenido del mensaje normalizado: remitente,
asunto, fecha, cuerpo en texto/HTML y la lista de adjuntos con sus metadatos.
Deja `mark_seen=False` para previsualizar sin alterar el estado leido/no-leido
del buzon (util en monitores que no deben "tocar" la bandeja del usuario).

## Gotchas

- Funcion impura: hace red sobre el `conn` vivo. Nunca lanza: comprueba `status`.
  El `conn` lo provee `imap_connect`; este grupo se compone en un mismo proceso
  Python (heredoc), no por `fn run`.
- Espera UID (de `imap_search`), NO numero de secuencia. Pasar un seq devuelve
  el mensaje equivocado o ninguno.
- `mark_seen=False` usa `BODY.PEEK[]` y NO marca leido; `mark_seen=True` usa
  `RFC822` y SI marca `\Seen`. Elige segun si quieres que el usuario vea el
  correo como ya leido.
- `attachments` lista metadatos (`filename`, `content_type`, `size_bytes`) pero
  NO incluye el binario para no inflar el resultado; `size_bytes` se mide
  decodificando el payload de esa parte. Para bajar un adjunto, haz un FETCH
  parcial aparte por su seccion.
- Charsets: cada parte de texto se decodifica con el charset declarado, con
  fallback a utf-8 y latin-1; las cabeceras (`Subject`, `From`, ...) se
  decodifican de RFC 2047 (`=?UTF-8?B?...?=`). Mensajes mal etiquetados pueden
  mostrar caracteres de reemplazo en vez de fallar.
- Mensajes muy grandes (adjuntos pesados) descargan el RFC822 completo: ten en
  cuenta el ancho de banda y la memoria.
- Cierra con `conn.logout()` al terminar (responsabilidad del caller).
