""" ============================================================================== Skript: enrich_fulltext_emails_v1.2.py Verze: 1.2 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.1: - S/MIME emaily (signed-data od Datove schranky, mBank, ComGate, PayU, ...): pokud unwrap_smime_v1.0 ulozil smime_body_text/smime_body_html, pouzije se PREFEROVANE pred bezvyznamnym vnejsim wrapper telem ("This is an S/MIME signed message"). Nazvy vnitrnich priloh (smime_inner_attachments) se pridavaji do attachments_summary, tj. dohledatelne pres find_attachment. - body_source: nova hodnota "smime" (rozbalene vnitrni telo). - EXTRACTOR_VERSION=1.2 -> vsechny existujici emaily v PG se preparsuji. Zmeny v1.1 vs v1.0: - Fallback poradi rozsireno o body_text (novy v parse_emails_graph_v1.4). - body_source umi novou hodnotu "text" (plne plain-text telo, max 2 MB). 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.2" 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