""" ============================================================================== Skript: enrich_fulltext_emails_v1.1.py Verze: 1.1 Datum: 2026-06-03 Autor: vladimir.buzalka Popis: Vytahne plny text z emailu ulozenych v MongoDB (db: emaily) a ulozi ho do PostgreSQL (db: MongoEmaily, tabulka: emails) s GIN tsvector indexem. Emaily se NESTAHUJI znovu - tela uz jsou v Mongo z parse_emails_graph_v1.4 (a refetch_text_bodies_v1.0 pro stare plain-text emaily). Tento skript jen vybere prvni dostupne telo a posle text do PG na fulltext. Zmeny proti v1.0: - Fallback poradi rozsireno: body_html -> body_text (novy v parse_emails_graph_v1.4) -> body_preview -> empty. Drive bylo body_html -> body_preview. - body_source umi novou hodnotu "text" (plne plain-text telo, max 2 MB). - EXTRACTOR_VERSION=1.1 -> vsechny existujici emaily v PG se preparsuji. Zdroj: MongoDB 192.168.1.76 db=emaily kolekce= (krome attachments_index) Cil: PostgreSQL 192.168.1.76 db=MongoEmaily tabulka=emails tsvector config 'soubory' (sdileny - simple + unaccent) Inkrementalita: Pokud (mailbox, message_id) jiz existuje a extractor_version je aktualni a modified_at v Mongo neni novejsi -> skip. Pri zmene verze extractoru se vse preparsuje. Spusteni: python enrich_fulltext_emails_v1.0.py # vsechny schranky python enrich_fulltext_emails_v1.0.py --mailbox vbuzalka@its.jnj.com python enrich_fulltext_emails_v1.0.py --limit 500 # test ============================================================================== """ from __future__ import annotations import argparse import re import sys import time import traceback from datetime import datetime, timezone from typing import Optional import psycopg from bs4 import BeautifulSoup from pymongo import MongoClient # --- konfigurace ------------------------------------------------------------ MONGO_URI = "mongodb://192.168.1.76:27017" MONGO_DB = "emaily" PG_DSN = ("host=192.168.1.76 port=5432 dbname=MongoEmaily " "user=vladimir.buzalka password=Vlado7309208104++") EXTRACTOR_VERSION = "1.1" MAX_TEXT_BYTES = 5 * 1024 * 1024 # plain text max 5 MB SKIP_COLLECTIONS = {"attachments_index"} BATCH_SIZE = 100 # --- SCHEMA ----------------------------------------------------------------- SCHEMA_SQL = """ CREATE EXTENSION IF NOT EXISTS unaccent; CREATE EXTENSION IF NOT EXISTS pg_trgm; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_ts_config WHERE cfgname = 'soubory') THEN CREATE TEXT SEARCH CONFIGURATION soubory ( COPY = simple ); ALTER TEXT SEARCH CONFIGURATION soubory ALTER MAPPING FOR hword, hword_part, word WITH unaccent, simple; END IF; END$$; CREATE TABLE IF NOT EXISTS emails ( id BIGSERIAL PRIMARY KEY, mailbox TEXT NOT NULL, message_id TEXT NOT NULL, graph_id TEXT, conversation_id TEXT, folder_path TEXT, subject TEXT, sender_email TEXT, sender_name TEXT, to_addrs TEXT, cc_addrs TEXT, bcc_addrs TEXT, sent_at TIMESTAMPTZ, received_at TIMESTAMPTZ, modified_at TIMESTAMPTZ, is_read BOOLEAN, is_draft BOOLEAN, has_attachments BOOLEAN, attachment_count INT, attachments_summary TEXT, body TEXT, body_length INT, body_source TEXT, -- 'html' | 'preview' | 'empty' tsv tsvector GENERATED ALWAYS AS ( to_tsvector('soubory'::regconfig, left( coalesce(subject, '') || ' ' || coalesce(sender_email, '') || ' ' || coalesce(sender_name, '') || ' ' || coalesce(to_addrs, '') || ' ' || coalesce(cc_addrs, '') || ' ' || coalesce(attachments_summary, '') || ' ' || coalesce(body, ''), 800000) ) ) STORED, extracted_at TIMESTAMPTZ DEFAULT now(), extractor_version TEXT, ok BOOLEAN, error TEXT, UNIQUE (mailbox, message_id) ); CREATE INDEX IF NOT EXISTS emails_tsv_gin ON emails USING gin(tsv); CREATE INDEX IF NOT EXISTS emails_subject_trgm ON emails USING gin(subject gin_trgm_ops); CREATE INDEX IF NOT EXISTS emails_sender_email_idx ON emails(sender_email); CREATE INDEX IF NOT EXISTS emails_mailbox_idx ON emails(mailbox); CREATE INDEX IF NOT EXISTS emails_received_idx ON emails(received_at DESC); CREATE INDEX IF NOT EXISTS emails_conv_idx ON emails(conversation_id); """ # --- HELPERY ---------------------------------------------------------------- _CTRL_RX = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]") _WS_RX = re.compile(r"[ \t]+") _NL_RX = re.compile(r"\n{3,}") def _clean_for_pg(s: str) -> str: if not s: return "" return _CTRL_RX.sub("", s) def _truncate(s: str) -> str: s = _clean_for_pg(s or "") if not s: return "" b = s.encode("utf-8", errors="replace") if len(b) <= MAX_TEXT_BYTES: return s return b[:MAX_TEXT_BYTES].decode("utf-8", errors="ignore") def html_to_text(html: str) -> str: """Extrahuje plain text z HTML emailu. Odstrani