--- 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 ' 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).