340 lines
12 KiB
Python
340 lines
12 KiB
Python
#!/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: Оператор Имя создал новый договор <b>номер</b>
|
||
pattern = r'Оператор\s+([А-Яа-я\s]+?)\s+создал\s+новый\s+договор\s+<b>(\d+)</b>'
|
||
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)
|
||
|
||
# Пробуем открыть папку с русским названием
|
||
try:
|
||
server.select_folder("Новые договоры")
|
||
except:
|
||
# Если не работает, пробуем кодированное имя
|
||
server.select_folder("&BB8EPgQ0BDoEOwROBEcENQQ9BDgETw-")
|
||
|
||
# Ищем непрочитанные
|
||
messages = server.search(['ALL'])
|
||
if not messages:
|
||
return []
|
||
|
||
new_emails = []
|
||
|
||
for msg_id in messages:
|
||
# Получаем письмо
|
||
data = server.fetch([msg_id], ['BODY[]', 'ENVELOPE'])
|
||
envelope = data[msg_id][b'ENVELOPE']
|
||
|
||
# Получаем тело
|
||
body_data = data[msg_id][b'BODY[]']
|
||
if body_data:
|
||
# Парсим тело
|
||
import email
|
||
msg = email.message_from_bytes(body_data)
|
||
|
||
body = ""
|
||
if msg.is_multipart():
|
||
for part in msg.walk():
|
||
if part.get_content_type() == "text/plain":
|
||
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')
|
||
|
||
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}")
|
||
|
||
# Помечаем как прочитанное
|
||
server.add_flags(msg_id, ['\\Seen'])
|
||
|
||
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() |