#!/usr/bin/env python3
"""
Gmail IMAP email exporter v3 — optimized.
Single IMAP query for all emails since Dec 20, 2025, then filter by domains in Python.
Each email becomes a PDF with body + attachments merged.
Organized by domain and also in a flat 'all' folder.
"""
import os
import sys
import re
import time
import logging
from pathlib import Path
from datetime import datetime, date
from email.utils import parsedate_to_datetime
from imap_tools import MailBox, AND
from weasyprint import HTML
from fpdf import FPDF
import fitz # PyMuPDF
# === CONFIG ===
IMAP_SERVER = "imap.gmail.com"
# Извлекаем учётку из .muttrc (сначала в проекте, потом в home)
MUTT_CONF = Path(__file__).parent / ".muttrc"
if not MUTT_CONF.exists():
MUTT_CONF = Path.home() / ".muttrc"
# Domains to search
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",
]
# From this date
SINCE_DATE = date(2025, 12, 20)
# Gmail folder to search
GMAIL_FOLDER = "[Gmail]/Вся почта"
# Output
BASE_OUTPUT = Path(__file__).parent / "exported_emails"
ALL_FOLDER = BASE_OUTPUT / "all"
# Progress tracking
PROGRESS_FILE = Path(__file__).parent / ".export_progress"
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(Path(__file__).parent / "export.log", encoding="utf-8"),
logging.StreamHandler(sys.stdout),
],
)
log = logging.getLogger(__name__)
def load_progress():
"""Load set of already processed UIDs."""
if PROGRESS_FILE.exists():
return set(PROGRESS_FILE.read_text().strip().splitlines())
return set()
def save_progress(uid, processed_set):
"""Save progress after each email."""
processed_set.add(str(uid))
PROGRESS_FILE.write_text("\n".join(processed_set))
def sanitize_filename(name, max_len=80):
"""Make filename safe."""
name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', '_', name)
name = re.sub(r'\s+', ' ', name).strip()
return name[:max_len]
def parse_mutt_config():
"""Extract IMAP user and password from .muttrc."""
config = {}
with open(MUTT_CONF) as f:
for line in f:
line = line.strip()
if line.startswith('#') or not line:
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:
key, val = match.group(1), match.group(2)
config[key] = val
return config
def html_to_pdf_stream(html_content):
"""Convert HTML to PDF bytes using weasyprint, fallback to fpdf."""
try:
html_doc = HTML(string=html_content)
return html_doc.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):
"""Fallback: strip HTML and use FPDF."""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html_content, 'lxml')
text = soup.get_text(separator='\n')
pdf = FPDF()
pdf.add_page()
pdf.add_font('DejaVu', '', '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', uni=True)
pdf.add_font('DejaVu', 'B', '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', uni=True)
pdf.set_font('DejaVu', '', 10)
lines = [line for line in text.split('\n') if line.strip()]
for line in lines:
pdf.multi_cell(0, 6, line)
pdf.ln(1)
return pdf.output()
def convert_attachment_to_pdf(attachment_data, filename, content_type):
"""Convert an attachment to PDF."""
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(attachment_data)
page_w = pdf.w - 20
scale = min(page_w / img_w, (pdf.h - 20) / img_h, 1.0)
w, h = img_w * scale, img_h * scale
import tempfile
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp:
tmp.write(attachment_data)
tmp_path = tmp.name
try:
pdf.image(tmp_path, x=10, y=10, w=w, h=h)
finally:
os.unlink(tmp_path)
return pdf.output()
elif ext in ('.doc', '.docx'):
import tempfile, subprocess
with tempfile.TemporaryDirectory() as tmpdir:
src = Path(tmpdir) / filename
src.write_bytes(attachment_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 attachment_data
elif ext in ('.xls', '.xlsx'):
import tempfile, subprocess
with tempfile.TemporaryDirectory() as tmpdir:
src = Path(tmpdir) / filename
src.write_bytes(attachment_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 in ('.txt', '.csv', '.log', '.xml', '.html', '.htm'):
try:
text = attachment_data.decode('utf-8', errors='replace')
except:
text = attachment_data.decode('latin-1', errors='replace')
return html_to_pdf_fpdf(text)
else:
info = f"Attachment: {filename}\nType: {content_type}\nSize: {len(attachment_data)} bytes\n\n[Binary content - not converted]"
return html_to_pdf_fpdf(info)
except Exception as e:
log.warning(f"Failed to convert {filename}: {e}")
info = f"Attachment: {filename}\nType: {content_type}\nSize: {len(attachment_data)} bytes\n\nConversion failed: {e}"
return html_to_pdf_fpdf(info)
def get_image_dims(data):
"""Get image dimensions."""
import io
from PIL import Image
img = Image.open(io.BytesIO(data))
w_px, h_px = img.size
w_mm = w_px * 25.4 / 96
h_mm = h_px * 25.4 / 96
return w_mm, h_mm
def merge_pdfs(pdf_bytes_list):
"""Merge multiple PDFs into one."""
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:
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
merger.insert_pdf(doc)
except Exception as e:
log.warning(f"Failed to merge a PDF section: {e}")
output = merger.tobytes()
merger.close()
return output
def build_email_html(msg):
"""Build nice HTML representation of email with headers."""
subject = msg.subject or "(Без темы)"
from_addr = msg.from_ or "unknown"
to_addr = ", ".join(msg.to) if msg.to else "unknown"
cc_addr = ", ".join(msg.cc) if msg.cc else ""
date_str = msg.date_str or str(msg.date) if msg.date else "unknown"
body_html = msg.html
if not body_html and msg.text:
text = msg.text
text_escaped = text.replace('&', '&').replace('<', '<').replace('>', '>')
body_html = text_escaped.replace('\n', '
\n')
elif not body_html:
body_html = "
(Нет тела письма)
" attachments_info = "" if msg.attachments: att_items = [] for att in msg.attachments: att_items.append(f'| От: | {from_addr} |
| Кому: | {to_addr} |
| Копия: | {cc_addr} |
| Дата: | {date_str} |