#!/usr/bin/env python3 from imapclient import IMAPClient import email import re import json import time import logging import os from datetime import datetime, timedelta from flask import Flask, request, jsonify from threading import Thread # ===== НАСТРОЙКИ ===== IMAP_SERVER = "mail.gtn.ru" IMAP_PORT = 993 EMAIL_USER = "M.Fedorov@gtn.ru" EMAIL_PASSWORD = "cloutfan8" FOLDER_NAME = "Новые договоры" DATA_DIR = "/opt/contract-parser/data" EMAILS_FILE = f"{DATA_DIR}/emails.json" SHOWN_FILE = f"{DATA_DIR}/shown_banners.json" SCRIPT_FILE = f"{DATA_DIR}/script.user.js" LOG_DIR = "/opt/contract-parser/logs" LOG_FILE = f"{LOG_DIR}/parser.log" # Настройка логирования logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(LOG_FILE), logging.StreamHandler() ] ) app = Flask(__name__) # ===== ФУНКЦИИ РАБОТЫ С ПОЧТОЙ ===== def decode_header_value(value): """Декодирует заголовки письма""" if not value: return "" decoded_parts = decode_header(value) result = "" for part, encoding in decoded_parts: if isinstance(part, bytes): if encoding: result += part.decode(encoding, errors='ignore') else: result += part.decode('utf-8', errors='ignore') else: result += str(part) return result def parse_email_body(body_text): """Извлекает имя оператора и номер договора из HTML письма""" # Ищем в HTML: Оператор Имя создал новый договор номер pattern = r'Оператор\s+([A-Za-zА-Яа-я\s]+?)\s+создал\s+новый\s+договор\s+(\d+)' match = re.search(pattern, body_text, re.IGNORECASE) if match: operator_name = match.group(1).strip() contract_number = match.group(2).strip() operator_name = re.sub(r'\s+', ' ', operator_name) return operator_name, contract_number return None, None def fetch_new_emails(): """Проверяет папку 'Новые договоры' и возвращает новые письма""" try: with IMAPClient(IMAP_SERVER, ssl=True) as server: server.login(EMAIL_USER, EMAIL_PASSWORD) server.select_folder("Новые договоры") # Ищем все письма (для теста), потом замените на ['UNSEEN'] messages = server.search(['ALL']) logging.info(f"Найдено писем в папке: {len(messages)}") if not messages: return [] new_emails = [] for msg_id in messages[-10:]: # последние 10 писем try: data = server.fetch([msg_id], ['BODY[]']) body_data = data[msg_id][b'BODY[]'] if not body_data: continue msg = email.message_from_bytes(body_data) # Извлекаем тело письма (HTML) body = "" if msg.is_multipart(): for part in msg.walk(): if part.get_content_type() == 'text/html': payload = part.get_payload(decode=True) if payload: body = payload.decode('utf-8', errors='ignore') break else: payload = msg.get_payload(decode=True) if payload: body = payload.decode('utf-8', errors='ignore') if not body: continue # Парсим имя и номер operator_name, contract_number = parse_email_body(body) if operator_name and contract_number: new_emails.append({ "contract_number": contract_number, "operator_name": operator_name, "received_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S") }) logging.info(f"✓ Найдено: договор {contract_number}, оператор {operator_name}") else: logging.debug(f"Не удалось распарсить письмо {msg_id}") except Exception as e: logging.error(f"Ошибка при обработке письма {msg_id}: {e}") continue # Помечаем письма как прочитанные for msg_id in messages[-10:]: try: server.add_flags(msg_id, ['\\Seen']) except: pass return new_emails except Exception as e: logging.error(f"Ошибка при подключении к почте: {e}") return [] # ===== ФУНКЦИИ РАБОТЫ С JSON ===== def load_emails(): if not os.path.exists(EMAILS_FILE): return {"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "emails": []} with open(EMAILS_FILE, 'r', encoding='utf-8') as f: return json.load(f) def save_emails(data): data["last_updated"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open(EMAILS_FILE, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) def load_shown(): if not os.path.exists(SHOWN_FILE): return {"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "shown": []} with open(SHOWN_FILE, 'r', encoding='utf-8') as f: return json.load(f) def save_shown(data): data["last_updated"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open(SHOWN_FILE, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) def cleanup_old_emails(): """Удаляет записи старше 4 месяцев""" four_months_ago = datetime.now() - timedelta(days=120) emails_data = load_emails() original_count = len(emails_data["emails"]) emails_data["emails"] = [ e for e in emails_data["emails"] if datetime.strptime(e["received_date"], "%Y-%m-%d %H:%M:%S") > four_months_ago ] if original_count != len(emails_data["emails"]): logging.info(f"Очистка emails: удалено {original_count - len(emails_data['emails'])} записей") save_emails(emails_data) shown_data = load_shown() original_shown = len(shown_data["shown"]) shown_data["shown"] = [ s for s in shown_data["shown"] if datetime.strptime(s["closed_at"], "%Y-%m-%d %H:%M:%S") > four_months_ago ] if original_shown != len(shown_data["shown"]): logging.info(f"Очистка shown: удалено {original_shown - len(shown_data['shown'])} записей") save_shown(shown_data) def add_new_emails(new_emails): """Добавляет новые письма в очередь (без дубликатов)""" if not new_emails: return emails_data = load_emails() shown_data = load_shown() shown_contracts = [s["contract_number"] for s in shown_data["shown"]] existing_contracts = [e["contract_number"] for e in emails_data["emails"]] added_count = 0 for email in new_emails: contract_number = email["contract_number"] if contract_number in shown_contracts: logging.info(f"Пропуск договора {contract_number} — уже был показан") continue if contract_number in existing_contracts: logging.info(f"Пропуск договора {contract_number} — уже в очереди") continue emails_data["emails"].append(email) added_count += 1 logging.info(f"Добавлен договор {contract_number} (оператор: {email['operator_name']})") if added_count > 0: save_emails(emails_data) # ===== FLASK ЭНДПОИНТЫ ===== @app.route('/get-banner', methods=['GET']) def get_banner(): contract_number = request.args.get('contract', '') if not contract_number: return jsonify({"error": "Не указан номер договора"}), 400 logging.info(f"Запрос плашки для договора {contract_number}") emails_data = load_emails() shown_data = load_shown() shown_contracts = [s["contract_number"] for s in shown_data["shown"]] if contract_number in shown_contracts: return jsonify({"show": False, "reason": "already_shown"}) for email in emails_data["emails"]: if email["contract_number"] == contract_number: return jsonify({ "show": True, "operator_name": email["operator_name"], "contract_number": email["contract_number"], "received_date": email["received_date"] }) return jsonify({"show": False, "reason": "not_found"}) @app.route('/close-banner', methods=['POST']) def close_banner(): data = request.get_json() contract_number = data.get('contract_number', '') if not contract_number: return jsonify({"error": "Не указан номер договора"}), 400 logging.info(f"Закрытие плашки для договора {contract_number}") emails_data = load_emails() shown_data = load_shown() # Находим оператора перед удалением operator_name = "" for email in emails_data["emails"]: if email["contract_number"] == contract_number: operator_name = email["operator_name"] break # Удаляем из очереди emails_data["emails"] = [ e for e in emails_data["emails"] if e["contract_number"] != contract_number ] save_emails(emails_data) # Добавляем в показанные shown_contracts = [s["contract_number"] for s in shown_data["shown"]] if contract_number not in shown_contracts: shown_data["shown"].append({ "contract_number": contract_number, "operator_name": operator_name, "closed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") }) save_shown(shown_data) cleanup_old_emails() return jsonify({"success": True}) @app.route('/script.user.js', methods=['GET']) def get_script(): script_path = "/opt/contract-parser/data/script.user.js" if os.path.exists(script_path): with open(script_path, 'r', encoding='utf-8') as f: script_content = f.read() return script_content, 200, {'Content-Type': 'application/javascript'} else: return jsonify({"error": "Скрипт не найден"}), 404 @app.route('/health', methods=['GET']) def health(): return jsonify({"status": "ok", "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}) # ===== ФОНОВЫЙ ПОТОК ===== def background_check(): logging.info("Фоновый поток запущен. Проверка почты каждую минуту.") while True: try: new_emails = fetch_new_emails() if new_emails: add_new_emails(new_emails) time.sleep(60) except Exception as e: logging.error(f"Ошибка в фоновом потоке: {e}") time.sleep(60) # ===== ЗАПУСК ===== def main(): logging.info("=" * 50) logging.info("Запуск сервера парсинга договоров") logging.info(f"IMAP сервер: {IMAP_SERVER}") logging.info(f"Папка: {FOLDER_NAME}") logging.info(f"Порт API: 8766") logging.info("=" * 50) os.makedirs(DATA_DIR, exist_ok=True) os.makedirs(LOG_DIR, exist_ok=True) if not os.path.exists(EMAILS_FILE): save_emails({"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "emails": []}) if not os.path.exists(SHOWN_FILE): save_shown({"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "shown": []}) logging.info("Первая проверка почты...") try: new_emails = fetch_new_emails() if new_emails: add_new_emails(new_emails) except Exception as e: logging.error(f"Ошибка при первой проверке: {e}") thread = Thread(target=background_check, daemon=True) thread.start() app.run(host='0.0.0.0', port=8766, debug=False, use_reloader=False) if __name__ == '__main__': main()