#!/usr/bin/env python3
"""
Safe SMTP/IMAP checker (Python 3)

- per-connection proxy via PySocks (no global monkey patch)
- proxy pre-check
- ThreadPool with semaphore control
- retry with exponential backoff
- IMAP verification (checks new UIDs with given SUBJECT)
- writes good SMTP lines to good.txt and logs to smtp_log.txt

Use only on servers/accounts you own or have explicit permission to test.
"""

import smtplib
import ssl
import socket
import socks        # PySocks
import imaplib
import email
import time
import random
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
from queue import Queue
from threading import Semaphore, Event, Thread, Lock
from datetime import datetime, timedelta
from colorama import init, Fore

init(autoreset=True)

# ---------- Configurable defaults ----------
MAX_WORKERS = 30              # max thread workers (user can change)
SEMAPHORE_LIMIT = 10          # concurrent active SMTP connections (per design)
CONNECT_TIMEOUT = 12          # socket connect timeout (seconds)
SMTP_OP_TIMEOUT = 18          # SMTP operation timeout
RETRY_COUNT = 2               # number of retries per SMTP (total attempts = RETRY_COUNT + 1)
BACKOFF_BASE = 1.5            # exponential backoff multiplier
PROXY_CHECK_TIMEOUT = 4       # timeout for proxy pre-check
IMAP_POLL_INTERVAL = 5        # seconds between IMAP polls
# -------------------------------------------

log_lock = Lock()

def log(msg, level="INFO"):
    ts = datetime.utcnow().isoformat()
    with log_lock:
        print(f"{Fore.CYAN}[{ts}] [{level}] {msg}")
        with open("smtp_log.txt", "a", encoding="utf-8") as lf:
            lf.write(f"{ts}\t{level}\t{msg}\n")

# ---------- Utilities for file loading ----------
def load_smtp_servers(filename):
    """
    Expects lines: host|port|user|password
    Returns list of 4-tuples
    """
    out = []
    with open(filename, "r", encoding="utf-8", errors="ignore") as f:
        for line in f:
            s = line.strip()
            if not s:
                continue
            parts = s.split("|")
            if len(parts) == 4:
                out.append((parts[0].strip(), parts[1].strip(), parts[2].strip(), parts[3].strip()))
            else:
                log(f"Skipping invalid SMTP line: {s}", "WARN")
    return out

def load_proxies(filename):
    """
    Expects lines: ip:port
    Returns list of strings ip:port
    """
    out = []
    with open(filename, "r", encoding="utf-8", errors="ignore") as f:
        for line in f:
            s = line.strip()
            if s:
                out.append(s)
    return out

# ---------- Proxy helpers ----------
PROXY_TYPE_MAP = {
    "http": socks.HTTP,
    "https": socks.HTTP,
    "socks4": socks.SOCKS4,
    "socks5": socks.SOCKS5
}

def parse_proxy_str(s):
    """Return (ip, port) or raise ValueError"""
    ip, port = s.split(":")
    return ip.strip(), int(port.strip())

def proxy_precheck(proxy_str, proxy_type, timeout=PROXY_CHECK_TIMEOUT):
    """Quick TCP connect through proxy to a public host to ensure liveliness.
       Uses socks.create_connection to open a connection (not global patch)."""
    try:
        ip, port = parse_proxy_str(proxy_str)
        proxy_kind = PROXY_TYPE_MAP.get(proxy_type.lower())
        if proxy_kind is None:
            return False
        # attempt to connect to 1.1.1.1:53 (DNS) via proxy quickly:
        dest = ("1.1.1.1", 53)
        # create a socks socket and set proxy
        s = socks.socksocket()
        s.set_proxy(proxy_kind, ip, port)
        s.settimeout(timeout)
        s.connect(dest)
        s.close()
        return True
    except Exception:
        return False

# ---------- SMTP per-connection (no global monkey patch) ----------
def smtp_send_with_proxy(smtp_info, to_email, subject, content, proxy_info=None, timeout=SMTP_OP_TIMEOUT):
    """
    smtp_info: (host, port, user, password)
    proxy_info: (proxy_type, proxy_str) or None
    Returns tuple (success_bool, error_message_or_none)
    """
    host, port_str, user, password = smtp_info
    port = int(port_str)
    context = ssl.create_default_context()
    sock = None
    smtp = None
    try:
        # Create a socket (socks if proxy_info provided)
        if proxy_info:
            proxy_type, proxy_str = proxy_info
            ip, p = parse_proxy_str(proxy_str)
            proxy_kind = PROXY_TYPE_MAP.get(proxy_type.lower())
            if proxy_kind is None:
                return False, f"Unsupported proxy type: {proxy_type}"
            s = socks.socksocket()
            s.set_proxy(proxy_kind, ip, p)
            s.settimeout(timeout)
            # connect to remote SMTP (TCP) via proxy
            s.connect((host, port))
            sock = s
        else:
            # plain socket
            s = socket.create_connection((host, port), timeout=timeout)
            sock = s

        # Wrap with SSL if implicit SSL port (465)
        if port == 465:
            ss = context.wrap_socket(sock, server_hostname=host)
            smtp = smtplib.SMTP_SSL()
            smtp.sock = ss
            smtp.file = smtp.sock.makefile("rb")
        else:
            # create SMTP object that will use our connected sock
            smtp = smtplib.SMTP()
            smtp.sock = sock
            smtp.file = smtp.sock.makefile("rb")
            # greet and then do starttls if supported
            smtp.helo()
            smtp.ehlo()
            try:
                smtp.starttls(context=context)
                smtp.ehlo()
            except Exception:
                # starttls may fail — continue without it
                pass

        smtp.login(user, password)
        # send mail
        from_addr = user
        msg = f"From: {from_addr}\r\nTo: {to_email}\r\nSubject: {subject}\r\n\r\n{content}"
        smtp.sendmail(from_addr, [to_email], msg)
        smtp.quit()
        return True, None
    except Exception as e:
        # ensure close of socket
        try:
            if smtp:
                smtp.close()
        except Exception:
            pass
        try:
            if sock:
                sock.close()
        except Exception:
            pass
        return False, str(e)

# ---------- Worker wrapper with retry/backoff ----------
def smtp_worker_task(smtp_info, to_email, subject, content, proxy_list, proxy_type, semaphore, result_queue):
    """One logical attempt with retries; uses semaphore to limit concurrency."""
    with semaphore:
        attempts = 0
        last_err = None
        # choose proxy or not
        use_proxy = bool(proxy_list and proxy_type)
        while attempts <= RETRY_COUNT:
            proxy_choice = None
            if use_proxy:
                proxy_choice = (proxy_type, random.choice(proxy_list))
            ok, err = smtp_send_with_proxy(smtp_info, to_email, subject, content, proxy_choice)
            if ok:
                result_queue.put(("success", smtp_info))
                log(f"Sent via {smtp_info[0]}:{smtp_info[1]} ({smtp_info[2]}) using proxy {proxy_choice}", "SUCCESS")
                return True
            else:
                last_err = err
                attempts += 1
                backoff = (BACKOFF_BASE ** attempts)
                log(f"Attempt {attempts} failed for {smtp_info[2]} -> {err}. Backing off {backoff:.1f}s", "WARN")
                time.sleep(backoff)
        # all retries exhausted
        result_queue.put(("failed", (smtp_info, last_err)))
        return False

# ---------- IMAP verifier (polling) ----------
def imap_verify_loop(imap_server, imap_user, imap_pass, subject, duration_minutes, result_queue, stop_event):
    """
    Connect to IMAP, poll for messages with given subject for duration_minutes.
    When a matching message found, put sender into result_queue as ('imap_success', sender)
    Checks only new UIDs to be efficient.
    """
    try:
        mail = imaplib.IMAP4_SSL(imap_server)
        mail.login(imap_user, imap_pass)
    except Exception as e:
        log(f"IMAP login failed: {e}", "ERROR")
        return

    try:
        mail.select("INBOX")
        since_time = datetime.utcnow()
        end_time = since_time + timedelta(minutes=duration_minutes)
        seen_uids = set()
        log(f"IMAP started: polling for subject '{subject}' until {end_time.isoformat()}", "INFO")
        while datetime.utcnow() < end_time and not stop_event.is_set():
            try:
                # Search for headers where SUBJECT matches exactly (use TEXT if header search fails)
                typ, data = mail.search(None, f'HEADER Subject "{subject}"')
                if typ == "OK":
                    uids = data[0].split()
                    for uid in uids:
                        if uid in seen_uids:
                            continue
                        seen_uids.add(uid)
                        typ2, msg_data = mail.fetch(uid, "(RFC822)")
                        if typ2 != "OK":
                            continue
                        raw = msg_data[0][1]
                        msg = email.message_from_bytes(raw)
                        frm = msg.get("From")
                        log(f"IMAP found message from: {frm}", "IMAP")
                        result_queue.put(("imap_success", frm))
                # else: nothing found or error – we just wait and retry
            except Exception as e:
                log(f"IMAP polling error: {e}", "WARN")
            time.sleep(IMAP_POLL_INTERVAL)
    finally:
        try:
            mail.logout()
        except Exception:
            pass
        log("IMAP polling finished", "INFO")

# ---------- Main program ----------
def main():
    print(Fore.YELLOW + "SAFE SMTP + IMAP checker (Python 3)")
    smtp_file = input("Enter SMTP servers filename: ").strip()
    smtp_servers = load_smtp_servers(smtp_file)
    if not smtp_servers:
        log("No SMTP servers loaded.", "ERROR")
        return

    to_email = input("Enter recipient email (you own): ").strip()
    subject = input("Enter email subject: ").strip()
    content = input("Enter email content: ").strip()

    # concurrency settings
    try:
        workers = int(input(f"Enter total worker threads (default {MAX_WORKERS}): ").strip() or MAX_WORKERS)
        sem_lim = int(input(f"Enter concurrent SMTP connections limit (default {SEMAPHORE_LIMIT}): ").strip() or SEMAPHORE_LIMIT)
    except ValueError:
        workers = MAX_WORKERS
        sem_lim = SEMAPHORE_LIMIT

    use_proxy = input("Użyć proxy? (y/n): ").strip().lower() == "y"
    proxy_list = []
    proxy_type = None

    if use_proxy:
        proxy_file = input("Podaj nazwę pliku z proxy (ip:port w każdej linii): ").strip()
        proxy_type = input("Typ proxy (http/socks4/socks5): ").strip().lower()

        # Wczytaj listę proxy
        proxy_candidates = load_proxies(proxy_file)
        log(f"Załadowano {len(proxy_candidates)} proxy. Rozpoczynam sprawdzanie...", "INFO")

        # Precheck proxy
        for p in proxy_candidates:
            if proxy_precheck(p, proxy_type):
                proxy_list.append(p)
            else:
                log(f"Proxy niedziałające: {p}", "WARN")

        log(f"Proxy działające po precheck: {len(proxy_list)}", "INFO")

        if not proxy_list:
            log("Brak działających proxy. Kontynuuję BEZ proxy.", "WARN")
            use_proxy = False

    # Prepare result queue and sets
    result_queue = Queue()
    success_set = set()
    failed_list = []

    semaphore = Semaphore(sem_lim)
    futures = []
    with ThreadPoolExecutor(max_workers=workers) as executor:
        for smtp_info in smtp_servers:
            futures.append(executor.submit(smtp_worker_task, smtp_info, to_email, subject, content,
                                           proxy_list if use_proxy else [], proxy_type if use_proxy else None,
                                           semaphore, result_queue))

        # Optionally: we can wait for all futures (or process results as they come)
        for fut in as_completed(futures):
            try:
                res = fut.result()
            except Exception as e:
                log(f"Worker raised: {e}", "ERROR")

    # Collect immediate successes / failures from result_queue
    while not result_queue.empty():
        tag, payload = result_queue.get()
        if tag == "success":
            success_set.add(tuple(payload))
        elif tag == "failed":
            failed_list.append(payload)

    log(f"Finished sending attempts. Successes: {len(success_set)} Failures: {len(failed_list)}", "INFO")

    # ---------- IMAP verification ----------
    imap_confirm = input("Do you want IMAP verification for actual delivery? (y/n): ").strip().lower() == "y"
    imap_results = set()
    if imap_confirm and success_set:
        imap_server = input("IMAP server (e.g. imap.example.com): ").strip()
        imap_user = input("IMAP user: ").strip()
        imap_pass = input("IMAP password: ").strip()
        duration_min = int(input("How many minutes to poll inbox (e.g. 5): ").strip() or 5)

        stop_event = Event()
        imap_thread = Thread(target=imap_verify_loop, args=(imap_server, imap_user, imap_pass, subject, duration_min, result_queue, stop_event))
        imap_thread.start()

        # wait for duration and collect any imap_success events
        imap_thread.join(timeout=duration_min * 60 + 5)
        stop_event.set()
        # drain queue
        while not result_queue.empty():
            tag, payload = result_queue.get()
            if tag == "imap_success":
                imap_results.add(payload)

    # Build good set - intersection of SMTP send successes and IMAP-found senders (if IMAP used)
    good_set = set()
    if imap_confirm and imap_results:
        # compare by sender email in IMAP (string contains real sender)
        for smtp_info in success_set:
            # smtp_info = (host,port,user,password)
            sender = smtp_info[2]
            # check if sender appears in any of imap_results strings
            for r in imap_results:
                if sender in r:
                    good_set.add("|".join(smtp_info))
    else:
        # if no IMAP verification, treat all successful sends as good
        for smtp_info in success_set:
            good_set.add("|".join(smtp_info))

    # Write good.txt
    with open("good.txt", "w", encoding="utf-8") as gf:
        for line in sorted(good_set):
            gf.write(line + "\n")

    log(f"Saved {len(good_set)} working SMTP entries to good.txt", "INFO")
    log("Done.", "INFO")


if __name__ == "__main__":
    main()
