435 lines
16 KiB
Python
435 lines
16 KiB
Python
import json
|
||
import time
|
||
import re
|
||
import os
|
||
import quopri
|
||
from imapclient import IMAPClient
|
||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||
from html.parser import HTMLParser
|
||
from collections import deque
|
||
from datetime import datetime
|
||
|
||
print("Сервер запущен, начало работы...", flush=True)
|
||
|
||
# === НАСТРОЙКИ ПОЧТЫ ===
|
||
IMAP_SERVER = "mail.gtn.ru"
|
||
EMAIL = "M.Fedorov@gtn.ru"
|
||
PASSWORD = "cloutfan8"
|
||
|
||
# Файлы
|
||
STATE_FILE = "processed_uids.json"
|
||
QUEUE_FILE = "leads_queue.json"
|
||
|
||
# Очередь лидов
|
||
MAX_QUEUE_SIZE = 50
|
||
leads_queue = deque(maxlen=MAX_QUEUE_SIZE)
|
||
processed_uids = set()
|
||
|
||
# ========== РАБОТА С ФАЙЛОМ ОЧЕРЕДИ ==========
|
||
def save_queue_to_file():
|
||
with open(QUEUE_FILE, 'w', encoding='utf-8') as f:
|
||
json.dump(list(leads_queue), f, ensure_ascii=False)
|
||
print(f"💾 Очередь сохранена: {len(leads_queue)} лидов")
|
||
|
||
def load_queue_from_file():
|
||
global leads_queue
|
||
if os.path.exists(QUEUE_FILE):
|
||
try:
|
||
with open(QUEUE_FILE, 'r', encoding='utf-8') as f:
|
||
saved_queue = json.load(f)
|
||
leads_queue = deque(saved_queue, maxlen=MAX_QUEUE_SIZE)
|
||
print(f"📂 Загружена очередь из файла: {len(leads_queue)} лидов")
|
||
except Exception as e:
|
||
print(f"Ошибка загрузки очереди: {e}")
|
||
|
||
# ========== ЗАГРУЗКА ОБРАБОТАННЫХ UID ==========
|
||
if os.path.exists(STATE_FILE):
|
||
try:
|
||
with open(STATE_FILE, 'r') as f:
|
||
processed_uids = set(json.load(f))
|
||
print(f"📂 Загружено {len(processed_uids)} обработанных UID")
|
||
except:
|
||
pass
|
||
|
||
def save_state():
|
||
with open(STATE_FILE, 'w') as f:
|
||
json.dump(list(processed_uids), f)
|
||
|
||
# ========== ПАРСИНГ HTML ==========
|
||
class HTMLToTextParser(HTMLParser):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.text = []
|
||
|
||
def handle_data(self, data):
|
||
if data.strip():
|
||
self.text.append(data.strip())
|
||
|
||
def handle_starttag(self, tag, attrs):
|
||
if tag in ['p', 'br', 'div', 'li', 'tr']:
|
||
if self.text and self.text[-1] != '':
|
||
self.text.append('')
|
||
|
||
def get_text(self):
|
||
return '\n'.join(self.text)
|
||
|
||
def html_to_text(html):
|
||
parser = HTMLToTextParser()
|
||
parser.feed(html)
|
||
text = parser.get_text()
|
||
lines = [line.strip() for line in text.split('\n') if line.strip()]
|
||
return '\n'.join(lines)
|
||
|
||
def decode_subject(subject):
|
||
if not subject:
|
||
return ""
|
||
if isinstance(subject, bytes):
|
||
try:
|
||
subject = subject.decode('utf-8')
|
||
except:
|
||
return str(subject)
|
||
if subject.startswith('=?UTF-8?B?'):
|
||
try:
|
||
import base64
|
||
encoded = subject.split('?B?')[1].split('?=')[0]
|
||
return base64.b64decode(encoded).decode('utf-8')
|
||
except:
|
||
return subject
|
||
return subject
|
||
|
||
def parse_email(subject, body_html):
|
||
body_html = decode_quoted_printable(body_html)
|
||
result = {"town": "", "phone": "", "email": "", "first_name": "", "surname": "", "second_name": "", "client_type": "", "addition": ""}
|
||
|
||
text_body = html_to_text(body_html)
|
||
|
||
print("=== ТЕКСТ ПОСЛЕ HTML_TO_TEXT ===")
|
||
print(repr(text_body[:500]))
|
||
print("================================")
|
||
|
||
plain_subject = decode_subject(subject)
|
||
|
||
# Вспомогательная функция: ищет метку и берёт значение из этой же или следующей строки
|
||
def extract_field(label):
|
||
lines = text_body.split('\n')
|
||
|
||
print("=== СТРОКИ ДЛЯ АНАЛИЗА ===")
|
||
for i, line in enumerate(lines[:20]):
|
||
print(f"{i}: {repr(line)}")
|
||
print("==========================")
|
||
|
||
for i, line in enumerate(lines):
|
||
# Ищем в одной строке: "Имя: Яна"
|
||
match = re.search(rf"{label}[:\s]+(.+)", line, re.IGNORECASE)
|
||
if match:
|
||
return match.group(1).strip()
|
||
# Ищем метку как отдельную строку и берём следующую
|
||
if re.search(rf"^{label}[:\s]*$", line.strip(), re.IGNORECASE):
|
||
if i + 1 < len(lines):
|
||
return lines[i + 1].strip()
|
||
return ""
|
||
|
||
# Извлекаем все поля
|
||
result["first_name"] = extract_field("Имя")
|
||
result["surname"] = extract_field("Фамилия")
|
||
result["second_name"] = extract_field("Отчество")
|
||
result["client_type"] = extract_field("Физ/Юр\. лицо")
|
||
|
||
# Телефон
|
||
phone_match = re.search(r"\+?7\d{10}|\+7\s\d{3}\s\d{3}\s\d{2}\s\d{2}|8\d{10}", text_body)
|
||
if not phone_match:
|
||
phone_val = extract_field("Телефон")
|
||
if phone_val:
|
||
phone_match = re.search(r"\+?7\d{10}|\+7\s\d{3}\s\d{3}\s\d{2}\s\d{2}|8\d{10}", phone_val)
|
||
if phone_match:
|
||
phone = re.sub(r'[^\d+]', '', phone_match.group(0))
|
||
if phone.startswith('8'):
|
||
phone = '+7' + phone[1:]
|
||
elif phone.isdigit() and len(phone) == 11:
|
||
phone = '+' + phone
|
||
result["phone"] = phone
|
||
|
||
# Email
|
||
email_match = re.search(r"Email[:\s]*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})", text_body, re.IGNORECASE)
|
||
if email_match:
|
||
result["email"] = email_match.group(1)
|
||
else:
|
||
email_val = extract_field("Email")
|
||
if email_val:
|
||
email_match = re.search(r"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})", email_val)
|
||
if email_match:
|
||
result["email"] = email_match.group(1)
|
||
|
||
# Город
|
||
towns = ["Гатчина", "Коммунар", "Сиверский", "Вырица", "Тайцы", "Дружная Горка"]
|
||
for town in towns:
|
||
if town.lower() in text_body.lower():
|
||
result["town"] = town
|
||
break
|
||
|
||
# Дата и время
|
||
result["received_at"] = datetime.now().strftime("%d.%m %H:%M")
|
||
|
||
# === НОВЫЙ ПОДХОД: собираем addition только из нужных полей ===
|
||
allowed_labels = ["Адрес", "Место подключения", "Дополнительно", "Комментарий"]
|
||
|
||
lines = text_body.split('\n')
|
||
addition_lines = []
|
||
i = 0
|
||
|
||
while i < len(lines):
|
||
line = lines[i].strip()
|
||
if not line:
|
||
i += 1
|
||
continue
|
||
|
||
# Проверяем, является ли строка разрешённой меткой
|
||
matched = False
|
||
for label in allowed_labels:
|
||
label_re = re.escape(label)
|
||
|
||
m = re.match(
|
||
rf"^{label_re}\s*:\s*(.*)$",
|
||
line,
|
||
re.IGNORECASE
|
||
)
|
||
|
||
if not m:
|
||
continue
|
||
|
||
value = m.group(1).strip()
|
||
|
||
# Значение указано в этой же строке
|
||
if value:
|
||
addition_lines.append(f"{label}: {value}")
|
||
# Значение находится на следующей строке
|
||
elif i + 1 < len(lines) and lines[i + 1].strip():
|
||
addition_lines.append(
|
||
f"{label}: {lines[i + 1].strip()}"
|
||
)
|
||
i += 1 # пропускаем следующую строку
|
||
else:
|
||
addition_lines.append(f"{label}:")
|
||
|
||
matched = True
|
||
break
|
||
|
||
# Если строка не является разрешённой меткой — пропускаем её
|
||
i += 1
|
||
|
||
# Собираем результат
|
||
if addition_lines:
|
||
cleaned_body = "\n".join(addition_lines)
|
||
else:
|
||
cleaned_body = ""
|
||
|
||
addition_final = f"Тема: {plain_subject}\n\n{cleaned_body}" if cleaned_body else f"Тема: {plain_subject}"
|
||
result["addition"] = addition_final
|
||
|
||
return result
|
||
def decode_quoted_printable(text):
|
||
try:
|
||
decoded_bytes = quopri.decodestring(text.encode('utf-8'))
|
||
return decoded_bytes.decode('utf-8', errors='ignore')
|
||
except:
|
||
return text
|
||
|
||
def fetch_new_emails():
|
||
print("Проверяю почту...", flush=True)
|
||
global processed_uids, leads_queue
|
||
try:
|
||
with IMAPClient(IMAP_SERVER, ssl=True) as server:
|
||
server.login(EMAIL, PASSWORD)
|
||
server.select_folder("Подключения")
|
||
|
||
messages = server.search(["ALL"])
|
||
if not messages:
|
||
return 0
|
||
|
||
ALLOWED_SENDERS = ["no_reply@gtn.ru"]
|
||
new_count = 0
|
||
for msg_id in messages:
|
||
if msg_id in processed_uids:
|
||
continue
|
||
|
||
data = server.fetch([msg_id], ["ENVELOPE", "BODY[]"])
|
||
envelope = data[msg_id][b"ENVELOPE"]
|
||
|
||
sender_email = ""
|
||
if envelope.from_:
|
||
from_addr = envelope.from_[0]
|
||
mailbox = from_addr.mailbox
|
||
host = from_addr.host
|
||
|
||
if isinstance(mailbox, bytes):
|
||
mailbox = mailbox.decode('utf-8')
|
||
if isinstance(host, bytes):
|
||
host = host.decode('utf-8')
|
||
|
||
if mailbox and host:
|
||
sender_email = f"{mailbox}@{host}"
|
||
|
||
print(f"📨 Найдено письмо от {sender_email}, UID={msg_id}")
|
||
|
||
allowed = False
|
||
for pattern in ALLOWED_SENDERS:
|
||
if pattern.startswith('@'):
|
||
if sender_email.endswith(pattern):
|
||
allowed = True
|
||
break
|
||
else:
|
||
if sender_email.lower() == pattern.lower():
|
||
allowed = True
|
||
break
|
||
|
||
if not allowed:
|
||
print(f"⏭️ Пропущено (отправитель не в списке)")
|
||
processed_uids.add(msg_id)
|
||
save_state()
|
||
continue
|
||
|
||
body_data = data[msg_id].get(b"BODY[]", b"")
|
||
subject = envelope.subject
|
||
if isinstance(subject, bytes):
|
||
subject = subject.decode('utf-8', errors='ignore')
|
||
elif subject is None:
|
||
subject = ""
|
||
|
||
body = body_data.decode("utf-8", errors="ignore")
|
||
body = decode_quoted_printable(body)
|
||
body_match = re.search(r"<body[^>]*>(.*?)</body>", body, re.DOTALL | re.IGNORECASE)
|
||
html_content = body_match.group(1) if body_match else body
|
||
|
||
|
||
# ВРЕМЕННАЯ ОТЛАДКА
|
||
print("=== СЫРОЙ HTML ===")
|
||
print(html_content[:1000])
|
||
print("=== ТЕКСТ ПОСЛЕ HTML_TO_TEXT ===")
|
||
print(html_to_text(html_content)[:1000])
|
||
print("==================")
|
||
|
||
lead = parse_email(subject, html_content)
|
||
leads_queue.append(lead)
|
||
save_queue_to_file()
|
||
processed_uids.add(msg_id)
|
||
save_state()
|
||
new_count += 1
|
||
|
||
print(f"✅ Взят лид от {sender_email}, телефон: {lead['phone']}")
|
||
|
||
return new_count
|
||
except Exception as e:
|
||
print(f"Ошибка IMAP: {e}")
|
||
return 0
|
||
|
||
class LeadHandler(BaseHTTPRequestHandler):
|
||
def do_GET(self):
|
||
global leads_queue
|
||
# Обработка статических .user.js файлов
|
||
if self.path.endswith(".user.js"):
|
||
try:
|
||
filename = self.path.split('/')[-1]
|
||
filepath = f"/home/yasu/scripts/{filename}"
|
||
with open(filepath, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
self.send_response(200)
|
||
self.send_header("Content-Type", "application/javascript")
|
||
self.send_header("Access-Control-Allow-Origin", "*")
|
||
self.end_headers()
|
||
self.wfile.write(content.encode('utf-8'))
|
||
return
|
||
except FileNotFoundError:
|
||
self.send_response(404)
|
||
self.end_headers()
|
||
return
|
||
if self.path == "/lead":
|
||
lead = leads_queue.popleft() if leads_queue else None
|
||
save_queue_to_file()
|
||
self.send_response(200)
|
||
self.send_header("Content-Type", "application/json")
|
||
self.send_header("Access-Control-Allow-Origin", "*")
|
||
self.end_headers()
|
||
self.wfile.write(json.dumps({
|
||
"lead": lead,
|
||
"queue_size": len(leads_queue)
|
||
}, ensure_ascii=False).encode('utf-8'))
|
||
elif self.path.startswith("/lead?index="):
|
||
try:
|
||
idx = int(self.path.split("=")[1])
|
||
if 0 <= idx < len(leads_queue):
|
||
lead = leads_queue[idx]
|
||
del leads_queue[idx]
|
||
save_queue_to_file()
|
||
else:
|
||
lead = None
|
||
self.send_response(200)
|
||
self.send_header("Content-Type", "application/json")
|
||
self.send_header("Access-Control-Allow-Origin", "*")
|
||
self.end_headers()
|
||
self.wfile.write(json.dumps({
|
||
"lead": lead,
|
||
"queue_size": len(leads_queue)
|
||
}, ensure_ascii=False).encode('utf-8'))
|
||
except:
|
||
self.send_response(400)
|
||
self.end_headers()
|
||
elif self.path == "/queue_list":
|
||
preview_list = []
|
||
for lead in leads_queue:
|
||
preview = {
|
||
"first_name": lead.get("first_name", ""),
|
||
"phone": lead.get("phone", ""),
|
||
"email": lead.get("email", ""),
|
||
"received_at": lead.get("received_at", "")
|
||
}
|
||
preview_list.append(preview)
|
||
self.send_response(200)
|
||
self.send_header("Content-Type", "application/json")
|
||
self.send_header("Access-Control-Allow-Origin", "*")
|
||
self.end_headers()
|
||
self.wfile.write(json.dumps({
|
||
"list": preview_list,
|
||
"size": len(leads_queue)
|
||
}, ensure_ascii=False).encode('utf-8'))
|
||
elif self.path == "/queue":
|
||
self.send_response(200)
|
||
self.send_header("Content-Type", "application/json")
|
||
self.send_header("Access-Control-Allow-Origin", "*")
|
||
self.end_headers()
|
||
self.wfile.write(json.dumps({"size": len(leads_queue)}).encode())
|
||
else:
|
||
self.send_response(404)
|
||
self.end_headers()
|
||
|
||
def do_OPTIONS(self):
|
||
self.send_response(200)
|
||
self.send_header("Access-Control-Allow-Origin", "*")
|
||
self.send_header("Access-Control-Allow-Methods", "GET")
|
||
self.end_headers()
|
||
|
||
def log_message(self, format, *args):
|
||
pass
|
||
|
||
def main():
|
||
# Загружаем сохранённую очередь
|
||
load_queue_from_file()
|
||
|
||
print("🚀 Сервер запущен на http://0.0.0.0:8765")
|
||
print(f"📁 Обработанных UID: {len(processed_uids)}")
|
||
print(f"📋 В очереди: {len(leads_queue)} лидов")
|
||
print("Ожидание писем...")
|
||
|
||
import threading
|
||
http_server = HTTPServer(("0.0.0.0", 8765), LeadHandler)
|
||
thread = threading.Thread(target=http_server.serve_forever, daemon=True)
|
||
thread.start()
|
||
|
||
while True:
|
||
new = fetch_new_emails()
|
||
if new > 0:
|
||
print(f"📬 Забрал {new} новое(ых) письмо(а). В очереди: {len(leads_queue)}")
|
||
time.sleep(300)
|
||
|
||
if __name__ == "__main__":
|
||
print("Запуск main()...", flush=True)
|
||
main() |