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, 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()