507 lines
21 KiB
Python
507 lines
21 KiB
Python
#!/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()
|