mutt/generate_email_stats_2026.py

507 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 = """
<html>
<head>
<meta charset='utf-8'>
<style>
@page {
size: A4 landscape;
margin: 0;
}
body {
font-family: 'DejaVu Sans', sans-serif;
margin: 0;
padding: 30px;
background: linear-gradient(135deg, #0b0f19 0%, #111827 100%);
color: #f3f4f6;
box-sizing: border-box;
width: 297mm;
height: 210mm;
}
.container {
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(17, 24, 39, 0.95);
border-radius: 20px;
padding: 25px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.6);
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 15px;
margin-bottom: 15px;
}
.header h1 {
margin: 0;
font-size: 24px;
font-weight: 700;
background: linear-gradient(to right, #00d2ff, #3a7bd5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: 0.5px;
}
.header .subtitle {
font-size: 13px;
color: #9ca3af;
margin-top: 4px;
}
.grid {
display: flex;
gap: 25px;
height: calc(100% - 75px);
}
.table-container {
flex: 2.3;
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: 11px;
}
th {
background: rgba(31, 41, 55, 0.8);
color: #38bdf8;
padding: 10px 12px;
font-weight: bold;
border-bottom: 2px solid rgba(255, 255, 255, 0.15);
text-transform: uppercase;
letter-spacing: 0.3px;
}
td {
padding: 8px 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
tr:nth-child(even) {
background: rgba(255, 255, 255, 0.02);
}
.domain-cell {
font-weight: bold;
color: #f3f4f6;
}
.inbound-text {
color: #10b981;
font-weight: 600;
}
.outbound-text {
color: #ef4444;
font-weight: 600;
}
.summary-container {
flex: 0.7;
background: rgba(31, 41, 55, 0.4);
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
border: 1px solid rgba(255, 255, 255, 0.06);
height: 100%;
box-sizing: border-box;
}
.summary-title {
font-size: 15px;
font-weight: bold;
color: #818cf8;
margin-bottom: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-box {
margin-bottom: 14px;
}
.stat-label {
font-size: 9.5px;
color: #9ca3af;
text-transform: uppercase;
margin-bottom: 2px;
letter-spacing: 0.3px;
}
.stat-value {
font-size: 22px;
font-weight: bold;
color: #f9fafb;
}
.stat-value.in {
color: #10b981;
}
.stat-value.out {
color: #ef4444;
}
.footer-info {
font-size: 9px;
color: #4b5563;
text-align: right;
line-height: 1.4;
}
</style>
</head>
<body>
<div class='container'>
<div class='header'>
<div>
<h1>⚖️ ОФИЦИАЛЬНАЯ ПЕРЕПИСКА С ВЕДОМСТВАМИ (2026 Г.)</h1>
<div class='subtitle'>Анализ входящих и исходящих официальных документов и страниц</div>
</div>
<div style='text-align: right;'>
<span style='font-size: 11px; color: #9ca3af;'>Период: 01.01.2026 - """ + date.today().strftime('%d.%m.%Y') + """</span>
</div>
</div>
<div class='grid'>
<div class='table-container'>
<table>
<thead>
<tr>
<th rowspan="2">Ведомство / Организация</th>
<th colspan="3" style="text-align: center; border-bottom: 1px solid rgba(255, 255, 255, 0.1);" class="inbound-text">📥 Входящие (мне)</th>
<th colspan="3" style="text-align: center; border-bottom: 1px solid rgba(255, 255, 255, 0.1);" class="outbound-text">📤 Исходящие (от меня)</th>
</tr>
<tr>
<th style="text-align: center;">Писем</th>
<th style="text-align: center;">Влож.</th>
<th style="text-align: center;">Стр.</th>
<th style="text-align: center;">Писем</th>
<th style="text-align: center;">Влож.</th>
<th style="text-align: center;">Стр.</th>
</tr>
</thead>
<tbody>
"""
for dom in sorted_domains:
in_m = grouped_stats[dom]['inbound']['mails']
in_a = grouped_stats[dom]['inbound']['atts']
in_p = grouped_stats[dom]['inbound']['pages']
out_m = grouped_stats[dom]['outbound']['mails']
out_a = grouped_stats[dom]['outbound']['atts']
out_p = grouped_stats[dom]['outbound']['pages']
if in_m == 0 and out_m == 0:
continue
html_content += f"""
<tr>
<td class='domain-cell'>{dom}</td>
<td style="text-align: center;" class="inbound-text">{in_m if in_m > 0 else '-'}</td>
<td style="text-align: center;">{in_a if in_a > 0 else '-'}</td>
<td style="text-align: center;">{in_p if in_p > 0 else '-'}</td>
<td style="text-align: center;" class="outbound-text">{out_m if out_m > 0 else '-'}</td>
<td style="text-align: center;">{out_a if out_a > 0 else '-'}</td>
<td style="text-align: center;">{out_p if out_p > 0 else '-'}</td>
</tr>
"""
html_content += f"""
<tr style="background: rgba(56, 189, 248, 0.08); font-weight: bold; border-top: 2px solid rgba(255, 255, 255, 0.15);">
<td style="color: #38bdf8;">ИТОГО:</td>
<td style="text-align: center;" class="inbound-text">{total_in_mails}</td>
<td style="text-align: center;">{total_in_atts}</td>
<td style="text-align: center;">{total_in_pages}</td>
<td style="text-align: center;" class="outbound-text">{total_out_mails}</td>
<td style="text-align: center;">{total_out_atts}</td>
<td style="text-align: center;">{total_out_pages}</td>
</tr>
</tbody>
</table>
</div>
<div class='summary-container'>
<div>
<div class='summary-title'>📊 Сводные данные</div>
<div class='stat-box'>
<div class='stat-label'>Всего писем</div>
<div class='stat-value' style='color: #38bdf8;'>{total_in_mails + total_out_mails}</div>
</div>
<div class='stat-box'>
<div class='stat-label'>Всего файлов / вложений</div>
<div class='stat-value'>{total_in_atts + total_out_atts} шт.</div>
</div>
<div class='stat-box'>
<div class='stat-label'>Общий объем документов</div>
<div class='stat-value' style='color: #818cf8;'>{total_in_pages + total_out_pages} стр.</div>
</div>
<div class='stat-box' style='border-top: 1px solid rgba(255, 255, 255, 0.08); padding-top: 8px;'>
<div class='stat-label'>Входящий поток</div>
<div class='stat-value inbound-text' style='font-size: 16px;'>{total_in_mails} писем / {total_in_pages} стр.</div>
</div>
<div class='stat-box'>
<div class='stat-label'>Исходящий поток</div>
<div class='stat-value outbound-text' style='font-size: 16px;'>{total_out_mails} писем / {total_out_pages} стр.</div>
</div>
</div>
<div class='footer-info'>
LKM37 CORE v4.3<br>
Сгенерировано ИИ Antigravity
</div>
</div>
</div>
</div>
</body>
</html>
"""
# Render HTML to PDF using WeasyPrint
print("Рендеринг HTML отчета в PDF...")
pdf_bytes = HTML(string=html_content).write_pdf()
# Convert PDF to PNG
print("Конвертация PDF в PNG...")
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
page = doc[0]
pix = page.get_pixmap(dpi=180)
png_bytes = pix.tobytes("png")
doc.close()
# Save the output PNG image
out_png_path = Path("/home/matrixhasyou/mutt/email_stats_2026.png")
out_png_path.write_bytes(png_bytes)
print(f"✅ Итоговый график сохранен: {out_png_path}")
# Send to Telegram
caption = (
f"📊 <b>Статистика переписки с ведомствами за весь 2026 год</b>\n\n"
f"🔹 Всего писем: <b>{total_in_mails + total_out_mails}</b>\n"
f"📥 Входящих: <b>{total_in_mails}</b> (вложений: {total_in_atts}, страниц: {total_in_pages})\n"
f"📤 Исходящих: <b>{total_out_mails}</b> (вложений: {total_out_atts}, страниц: {total_out_pages})\n"
f"📁 Общий объем архива: <b>{total_in_pages + total_out_pages} страниц</b>"
)
send_telegram_photo(png_bytes, caption)
if __name__ == "__main__":
main()