#!/usr/bin/env python3 """ Gmail Email Auditor v2.6 SPEEDSTER for Domovoy Bot. 1. Fast header-only scan to find matching UIDs. 2. Full download only for matches (with attachments). 3. Merges all PDFs into a master archive for NotebookLM. 4. Updates domovoy.db and notifies via Telegram. """ import os import sys import re import time import logging import sqlite3 import tempfile import subprocess from pathlib import Path from datetime import datetime, date, timedelta, timezone import requests from imap_tools import MailBox, AND from weasyprint import HTML import fitz # PyMuPDF from PIL import Image from bs4 import BeautifulSoup from dotenv import load_dotenv # === CONFIG & PATHS === BASE_DIR = Path(__file__).resolve().parent PROJECT_ROOT = BASE_DIR.parent.parent load_dotenv(PROJECT_ROOT / ".env") DB_PATH = PROJECT_ROOT / "database" / "domovoy.db" MUTT_CONF = BASE_DIR / ".muttrc" EXPORT_DIR = BASE_DIR / "exported_emails" ALL_FOLDER = EXPORT_DIR / "all" MASTER_PDF = EXPORT_DIR / "TOTAL_ARCHIVE_2025-2026.pdf" PROGRESS_FILE = BASE_DIR / ".export_progress" IMAP_SERVER = "imap.gmail.com" SINCE_DATE = date(2025, 12, 20) DOMAINS = [ "gosuslugi.ru", "dom.gosuslugi.ru", "gov.ru", "duma.gov.ru", "mvd.gov.ru", "mvd.ru", "rkn.gov.ru", "nalog.gov.ru", "nalog.ru", "genproc.gov.ru", "pfr.gov.ru", "fss.ru", "rospotrebnadzor.ru", "russianpost.ru", "mailop.ru", "73.mailop.ru", "uk-service.ru", "ulgss.ru", "ulgov.ru", "ulgkh.ru", "dgi.ru", "mos.ru", "pfrf.ru", "fsin.gov.ru", "skrf.ru", "epp.genproc.gov.ru", "ugpr.ru", "fas.gov.ru", "minstroyrf.gov.ru", "gkh.ru", "sudrf.ru", "msud.ru", "arbitr.ru", "fssp.gov.ru", "fssprus.ru", "mirsud", "msud73.ru", "mirsud73.ru", "court.ru", "sud.ru", "vkks.ru" ] KEYWORDS = ["суд", "судье", "судебн", "дело №", "иск", "заседание"] # Bot notification config BOT_TOKEN = os.getenv("BOT_TOKEN") ADMIN_USER_ID = os.getenv("ADMIN_USER_ID") # Logging configuration with Ulyanovsk timezone (UTC+4) ULY_TZ_OFFSET = 4 * 3600 class UlyanovskFormatter(logging.Formatter): def converter(self, timestamp): return datetime.fromtimestamp(timestamp, tz=timezone(timedelta(hours=4))).timetuple() def formatTime(self, record, datefmt=None): dt = datetime.fromtimestamp(record.created, tz=timezone(timedelta(hours=4))) return dt.strftime(datefmt) if datefmt else dt.isoformat() log_formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s') # We'll use a simpler way - just set the converter for the formatter log_formatter.converter = lambda *args: (datetime.now(timezone(timedelta(hours=4))).timetuple()) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.FileHandler(BASE_DIR / "auditor_run.log", encoding="utf-8"), logging.StreamHandler(sys.stdout), ], ) # Force all handlers to use Ulyanovsk time in their timestamps for handler in logging.root.handlers: handler.formatter.converter = lambda *args: (datetime.now(timezone(timedelta(hours=4))).timetuple()) log = logging.getLogger("EmailAuditor") # === UTILS === def parse_mutt_config(): config = {} if not MUTT_CONF.exists(): return config with open(MUTT_CONF) as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue match = re.match(r'set\s+(\w+)\s*=\s*"([^"]*)"', line) if not match: match = re.match(r'set\s+(\w+)\s*=\s*(\S+)', line) if match: config[match.group(1)] = match.group(2) return config def sanitize_filename(name): name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', '_', name) name = re.sub(r'\s+', ' ', name).strip() return name[:80] def send_bot_message(text): if not BOT_TOKEN or not ADMIN_USER_ID: return url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage" try: proxies = None if os.getenv("USE_PROXY", "").lower() == "true": proxy_url = f"socks5://{os.getenv('PROXY_HOST', '127.0.0.1')}:{os.getenv('PROXY_PORT', '10808')}" proxies = {"http": proxy_url, "https": proxy_url} requests.post(url, json={"chat_id": ADMIN_USER_ID, "text": text, "parse_mode": "HTML"}, proxies=proxies, timeout=10) except Exception as e: log.error(f"Failed to send Telegram notification: {e}") # === PDF GENERATION === def html_to_pdf_stream(html_content): try: return HTML(string=html_content).write_pdf() except Exception as e: log.warning(f"weasyprint failed, using fpdf fallback: {e}") return html_to_pdf_fpdf(html_content) def html_to_pdf_fpdf(html_content): from fpdf import FPDF soup = BeautifulSoup(html_content, 'lxml') text = soup.get_text(separator='\n') pdf = FPDF() pdf.add_page() font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" if not os.path.exists(font_path): font_path = "/usr/share/fonts/dejavu/DejaVuSans.ttf" if os.path.exists(font_path): pdf.add_font('DejaVu', '', font_path) pdf.set_font('DejaVu', '', 10) else: pdf.set_font('helvetica', '', 10) for line in [l for l in text.split('\n') if l.strip()]: try: pdf.multi_cell(0, 6, line) pdf.ln(1) except: pdf.multi_cell(0, 6, line.encode('latin-1', 'replace').decode('latin-1')) pdf.ln(1) return pdf.output() def get_image_dims(data): import io img = Image.open(io.BytesIO(data)) return img.size[0] * 25.4 / 96, img.size[1] * 25.4 / 96 def convert_attachment_to_pdf(data, filename, content_type): from fpdf import FPDF ext = Path(filename).suffix.lower() try: if ext in ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', '.svg'): pdf = FPDF(); pdf.add_page() img_w, img_h = get_image_dims(data) scale = min((pdf.w - 20) / img_w, (pdf.h - 20) / img_h, 1.0) with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp: tmp.write(data); tmp_path = tmp.name try: pdf.image(tmp_path, x=10, y=10, w=img_w * scale, h=img_h * scale) finally: os.unlink(tmp_path) return pdf.output() elif ext in ('.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.odt', '.ods'): with tempfile.TemporaryDirectory() as tmpdir: src = Path(tmpdir) / filename src.write_bytes(data) subprocess.run(['libreoffice', '--headless', '--convert-to', 'pdf', '--outdir', tmpdir, str(src)], capture_output=True, timeout=120) pdf_path = src.with_suffix('.pdf') if pdf_path.exists(): return pdf_path.read_bytes() elif ext == '.pdf': return data elif ext in ('.txt', '.csv', '.log', '.xml', '.html', '.htm', '.json'): try: text = data.decode('utf-8', errors='replace') except: text = data.decode('latin-1', errors='replace') return html_to_pdf_fpdf(text) return None except Exception as e: log.warning(f"Failed to convert {filename}: {e}") return None def merge_pdfs(pdf_bytes_list): if not pdf_bytes_list: return None if len(pdf_bytes_list) == 1: return pdf_bytes_list[0] merger = fitz.open() for pdf_bytes in pdf_bytes_list: try: merger.insert_pdf(fitz.open(stream=pdf_bytes, filetype="pdf")) except Exception as e: log.warning(f"Merge error: {e}") output = merger.tobytes(); merger.close() return output def build_email_html(msg): subject = msg.subject or "(Без темы)" from_addr = msg.from_ or "unknown" to_addr = ", ".join(msg.to) if msg.to else "unknown" date_str = msg.date_str or str(msg.date) if msg.date else "unknown" body_html = msg.html or (f"
{msg.text}" if msg.text else "(Нет тела письма)
") return f"От: {from_addr}
Кому: {to_addr}
Дата: {date_str}