diff --git a/email_stats_2026.png b/email_stats_2026.png new file mode 100644 index 0000000..3c3c8bd Binary files /dev/null and b/email_stats_2026.png differ diff --git a/fetch_and_build_remaining_laws.py b/fetch_and_build_remaining_laws.py new file mode 100755 index 0000000..70dac3f --- /dev/null +++ b/fetch_and_build_remaining_laws.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +""" +Скрипт автономной сборки официальных полных текстов действующих нормативно-правовых актов РФ +с портала КонсультантПлюс в структурированные PDF через wkhtmltopdf. + +Собираемые документы: +1. Постановление Правительства РФ № 354 (cons_doc_LAW_114247) +2. Постановление Госстроя РФ № 170 (cons_doc_LAW_44772) +3. СанПиН 2.1.3684-21 (cons_doc_LAW_376166) +4. Приказ Генеральной прокуратуры РФ № 45 (cons_doc_LAW_141740) +""" + +import os +import sys +import time +import subprocess +import urllib.request +from bs4 import BeautifulSoup +import fitz + +HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} + +DOCS = [ + { + 'law_id': 'cons_doc_LAW_114247', + 'out_name': '04_ПП_РФ_354_Коммунальные_Услуги_Полный.pdf', + 'title_prefix': 'Постановление Правительства РФ от 06.05.2011 № 354' + }, + { + 'law_id': 'cons_doc_LAW_44772', + 'out_name': '06_Постановление_Госстроя_170_Эксплуатация_Жилфонда_Полный.pdf', + 'title_prefix': 'Постановление Госстроя РФ от 27.09.2003 № 170 (МДК 2-03.2003)' + }, + { + 'law_id': 'cons_doc_LAW_376166', + 'out_name': '07_СанПиН_2.1.3684-21_Санитарные_Требования_МКД_Полный.pdf', + 'title_prefix': 'СанПиН 2.1.3684-21 (Постановление Главного санврача РФ № 3)' + }, + { + 'law_id': 'cons_doc_LAW_141740', + 'out_name': '08_Приказ_Генпрокуратуры_45_Инструкция_Рассмотрения_Обращений_Полный.pdf', + 'title_prefix': 'Приказ Генеральной прокуратуры РФ от 30.01.2013 № 45' + } +] + +DEST_DIR = '/home/matrixhasyou/mutt/legal_sources' +os.makedirs(DEST_DIR, exist_ok=True) + +def build_law(law_info): + law_id = law_info['law_id'] + out_pdf = os.path.join(DEST_DIR, law_info['out_name']) + base_url = f'https://www.consultant.ru/document/{law_id}/' + + print(f"\n=======================================================") + print(f"[*] Сборка документа: {law_info['title_prefix']} ({law_id})") + print(f"=======================================================") + + req = urllib.request.Request(base_url, headers=HEADERS) + try: + with urllib.request.urlopen(req, timeout=15) as resp: + soup = BeautifulSoup(resp.read().decode('utf-8', errors='ignore'), 'html.parser') + except Exception as e: + print(f"[-] Ошибка загрузки оглавления {base_url}: {e}") + return False + + raw_title = soup.find('title').text + main_title = raw_title.split('\\')[0].strip() if '\\' in raw_title else raw_title.strip() + + links = [] + seen = set() + for a in soup.find_all('a', href=True): + href = a['href'] + if href.startswith(f'/document/{law_id}/') and len(href) > len(f'/document/{law_id}/'): + clean_href = href.split('#')[0] + if clean_href not in seen: + seen.add(clean_href) + links.append((clean_href, a.text.strip())) + + print(f"[+] Найдено разделов/статей: {len(links)}") + + html_parts = [f""" + +
+ + + + +{t}
') + elif elem.name == 'table': + html_parts.append(str(elem)) + time.sleep(0.2) + except Exception as e: + print(f" [!] Ошибка загрузки раздела {href}: {e}") + + html_parts.append('') + + tmp_html = f'/tmp/{law_id}.html' + with open(tmp_html, 'w', encoding='utf-8') as f: + f.write('\n'.join(html_parts)) + + print(f"[*] Компиляция PDF через wkhtmltopdf: {out_pdf}...") + subprocess.run([ + 'wkhtmltopdf', + '--encoding', 'utf-8', + '--margin-top', '20mm', + '--margin-bottom', '20mm', + '--margin-left', '15mm', + '--margin-right', '15mm', + tmp_html, out_pdf + ], check=True) + + try: + doc = fitz.open(out_pdf) + pages_count = len(doc) + except Exception: + pages_count = 0 + + size_kb = os.path.getsize(out_pdf) / 1024 + print(f"[✔] УСПЕШНО СОЗДАН: {out_pdf} ({size_kb:.1f} КБ, {pages_count} страниц)") + return True + +def main(): + for doc_info in DOCS: + build_law(doc_info) + print("\n[🎉] ВСЕ ЗАКОНЫ РФ ПОЛНОСТЬЮ СОБРАНЫ В PDF!") + +if __name__ == '__main__': + main() diff --git a/generate_email_stats_2026.py b/generate_email_stats_2026.py new file mode 100644 index 0000000..23a4fa3 --- /dev/null +++ b/generate_email_stats_2026.py @@ -0,0 +1,507 @@ +#!/usr/bin/env python3 +import os +import sys +import re +import fitz # PyMuPDF +from datetime import date, datetime +from pathlib import Path +import requests +from weasyprint import HTML + +# Telegram settings +BOT_TOKEN = "8725618164:AAH1tGalq-pw1l0t4P5c0sdLkCVJdh7IE7M" +ADMIN_USER_ID = "197957361" + +# Domain mapping for human-readable naming +DOMAINS_MAP = { + "gosuslugi.ru": "Госуслуги РФ", + "dom.gosuslugi.ru": "ГИС ЖКХ", + "sudrf.ru": "Судебная система РФ", + "zavolgskiy.uln@sudrf.ru": "Заволжский районный суд", + "ulgss.ru": "УО Жилстройсервис", + "ulgkh.ru": "Департамент ЖКХ", + "fssp.gov.ru": "ФССП России", + "fssprus.ru": "ФССП России", + "gov.ru": "Правительство РФ", + "ulgov.ru": "Администрация Ульяновской обл.", + "russianpost.ru": "Почта России", + "rkn.gov.ru": "Роскомнадзор", + "nalog.gov.ru": "ФНС России", + "nalog.ru": "ФНС России", + "genproc.gov.ru": "Прокуратура РФ", + "epp.genproc.gov.ru": "Прокуратура РФ", + "mvd.gov.ru": "МВД России", + "mvd.ru": "МВД России", + "rospotrebnadzor.ru": "Роспотребнадзор", + "pfr.gov.ru": "ПФР / СФР России", + "pfrf.ru": "ПФР / СФР России", + "fss.ru": "Фонд социального страхования", + "sledcom.ru": "Следственный комитет РФ", + "vkks.ru": "ВККС РФ", + "mailop.ru": "Почтовый шлюз Mailop", + "73.mailop.ru": "Почтовый шлюз 73.Mailop", + "uk-service.ru": "УК Сервис-М", + "dgi.ru": "Департамент гос. имущества", + "mos.ru": "Портал Москвы mos.ru", + "fsin.gov.ru": "ФСИН России", + "skrf.ru": "Следственный комитет РФ", + "ugpr.ru": "Управление госзакупок", + "fas.gov.ru": "ФАС России", + "minstroyrf.gov.ru": "Минстрой РФ", + "gkh.ru": "Портал ЖКХ", + "msud.ru": "Мировые судьи РФ", + "arbitr.ru": "Арбитражные суды РФ", + "mirsud": "Мировые судьи", + "msud73.ru": "Мировые судьи Ульяновской обл.", + "mirsud73.ru": "Мировые судьи Ульяновской обл.", + "court.ru": "Судебный департамент РФ", + "sud.ru": "Судебный департамент РФ", + "uloblsud.ru": "Ульяновский областной суд", + "duma.gov.ru": "Государственная Дума РФ", +} + +def send_telegram_photo(photo_bytes, caption): + url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendPhoto" + files = { + "photo": ("stats.png", photo_bytes, "image/png") + } + data = { + "chat_id": ADMIN_USER_ID, + "caption": caption, + "parse_mode": "HTML" + } + + print("Отправка отчета в Telegram...") + try: + r = requests.post(url, data=data, files=files, timeout=30) + if r.status_code == 200: + print("🎉 Картинка со статистикой успешно отправлена!") + else: + print(f"❌ Ошибка отправки: {r.status_code} - {r.text}") + except Exception as e: + print(f"❌ Ошибка отправки запроса в Telegram: {e}") + +def main(): + folder = Path("/home/matrixhasyou/mutt/exported_emails/all") + if not folder.exists(): + print(f"Папка не существует: {folder}") + sys.exit(1) + + pdf_files = list(folder.glob("*.pdf")) + print(f"Всего PDF файлов найдено: {len(pdf_files)}") + + stats = {} + + # Summary counters + total_in_mails = 0 + total_in_pages = 0 + total_in_atts = 0 + total_out_mails = 0 + total_out_pages = 0 + total_out_atts = 0 + + # Process files + for filepath in pdf_files: + filename = filepath.name + + # Parse filename + # Format is typically: {match_domain}_{date_part}_{safe_from}_{safe_subject}.pdf + # Let's extract date + date_match = re.search(r'_(\d{4}-\d{2}-\d{2})_', filename) + if not date_match: + # Maybe it starts with date: YYYY-MM-DD_domain_... + date_match = re.match(r'^(\d{4}-\d{2}-\d{2})_', filename) + + if not date_match: + continue + + date_str = date_match.group(1) + if not date_str.startswith("2026"): + continue + + # Parse domain and sender + parts = filename.split("_", 2) + if len(parts) >= 2: + # If domain first + domain = parts[0] + # Verify if it looks like domain + if not ("." in domain or "судебное" in domain or "other" in domain): + # Fallback to general parsing + domain = "other" + else: + domain = "other" + + try: + doc = fitz.open(filepath) + num_pages = len(doc) + first_page_text = doc[0].get_text() + + # Determine direction (inbound / outbound) + is_outbound = False + first_page_lower = first_page_text.lower() + + # Heuristics + if "от: ktybdsq@gmail.com" in first_page_lower or "from: ktybdsq@gmail.com" in first_page_lower: + is_outbound = True + elif "_ktybdsq_" in filename.lower(): + is_outbound = True + elif "fwd_" in filename.lower() and "от: ktybdsq@gmail.com" in first_page_lower: + is_outbound = True + + # Attachment count + att_count = 0 + att_match = re.search(r"(?:Вложения|Attachments):(.*)", first_page_text, re.DOTALL | re.IGNORECASE) + if att_match: + block_text = att_match.group(1) + # Count bytes occurrences as an indicator of attachments + att_count = len(re.findall(r"bytes\)", block_text)) + + doc.close() + + # Add to stats + if domain not in stats: + stats[domain] = { + 'inbound': {'mails': 0, 'pages': 0, 'atts': 0}, + 'outbound': {'mails': 0, 'pages': 0, 'atts': 0} + } + + direction = 'outbound' if is_outbound else 'inbound' + stats[domain][direction]['mails'] += 1 + stats[domain][direction]['pages'] += num_pages + stats[domain][direction]['atts'] += att_count + + if is_outbound: + total_out_mails += 1 + total_out_pages += num_pages + total_out_atts += att_count + else: + total_in_mails += 1 + total_in_pages += num_pages + total_in_atts += att_count + + except Exception as e: + print(f"Ошибка парсинга {filename}: {e}") + + # Group results to consolidate subdomains or same entities + grouped_stats = {} + for dom, data in stats.items(): + mapped_name = DOMAINS_MAP.get(dom, dom) + + # General groupings for cleaner output + if "sudrf.ru" in dom or "mirsud" in dom or "msud" in dom or "court" in dom or "arbitr" in dom: + mapped_name = "Судебная система РФ" + elif "gosuslugi.ru" in dom: + mapped_name = "Портал Госуслуг РФ" + elif "gov.ru" in dom or "ulgov.ru" in dom: + mapped_name = "Органы власти / Ведомства" + elif "fssp.gov.ru" in dom or "fssprus.ru" in dom: + mapped_name = "ФССП (Судебные приставы)" + elif "nalog.ru" in dom or "nalog.gov.ru" in dom: + mapped_name = "ФНС (Налоговая служба)" + + if mapped_name not in grouped_stats: + grouped_stats[mapped_name] = { + 'inbound': {'mails': 0, 'pages': 0, 'atts': 0}, + 'outbound': {'mails': 0, 'pages': 0, 'atts': 0} + } + + for dir_key in ['inbound', 'outbound']: + for val_key in ['mails', 'pages', 'atts']: + grouped_stats[mapped_name][dir_key][val_key] += data[dir_key][val_key] + + # Sort domains by total mail activity + sorted_domains = sorted( + grouped_stats.keys(), + key=lambda k: grouped_stats[k]['inbound']['mails'] + grouped_stats[k]['outbound']['mails'], + reverse=True + ) + + # Render HTML to PDF and PNG + html_content = """ + + + + + + +| Ведомство / Организация | +📥 Входящие (мне) | +📤 Исходящие (от меня) | +||||
|---|---|---|---|---|---|---|
| Писем | +Влож. | +Стр. | +Писем | +Влож. | +Стр. | +|
| {dom} | +{in_m if in_m > 0 else '-'} | +{in_a if in_a > 0 else '-'} | +{in_p if in_p > 0 else '-'} | +{out_m if out_m > 0 else '-'} | +{out_a if out_a > 0 else '-'} | +{out_p if out_p > 0 else '-'} | +
| ИТОГО: | +{total_in_mails} | +{total_in_atts} | +{total_in_pages} | +{total_out_mails} | +{total_out_atts} | +{total_out_pages} | +