"""One-off: store inline (cid:) images that the old shallow attachment extractor missed for messages nested as mixed>related>alternative, so their cid: references resolve instead of rendering broken. Idempotent — skips content_ids already stored. Run: python -m scripts.backfill_inline_attachments """ from __future__ import annotations import asyncio from sqlalchemy import select from app.db import get_sessionmaker from app.indexer import blobstore from app.indexer.mime import parse_message from app.models import Attachment, Message async def main() -> None: sm = get_sessionmaker() added = 0 fixed_msgs = 0 async with sm() as session: msgs = list(await session.scalars(select(Message))) for msg in msgs: try: raw = blobstore.load(msg.user_id, msg.blob_sha256) except FileNotFoundError: continue parsed = parse_message(raw) inline = [a for a in parsed.attachments if a.content_id] if not inline: continue have = set( await session.scalars( select(Attachment.content_id).where( Attachment.message_id == msg.id, Attachment.content_id.is_not(None) ) ) ) new = 0 for att in inline: if att.content_id in have: continue att_sha = blobstore.store(msg.user_id, att.payload) session.add( Attachment( message_id=msg.id, user_id=msg.user_id, filename=att.filename, mime_type=att.mime_type, size_bytes=len(att.payload), content_id=att.content_id, blob_sha256=att_sha, ) ) new += 1 if new: added += new fixed_msgs += 1 await session.commit() print(f"stored {added} missing inline images across {fixed_msgs} messages") if __name__ == "__main__": asyncio.run(main())