import asyncio import socket import ssl import smtplib from dataclasses import dataclass from typing import List, Dict, Any import dns.resolver from app.config import settings from datetime import datetime @dataclass class MXRecord: preference: int exchange: str async def resolve_mx(domain: str) -> List[MXRecord]: def _sync() -> List[MXRecord]: r = dns.resolver.Resolver() r.timeout = settings.DNS_TIMEOUT r.lifetime = settings.DNS_LIFETIME answers = r.resolve(domain, 'MX') recs = [] for rr in answers: recs.append(MXRecord(preference=int(rr.preference), exchange=str(rr.exchange).rstrip('.'))) recs.sort(key=lambda x: x.preference) return recs return await asyncio.to_thread(_sync) async def resolve_txt(domain: str) -> List[str]: def _sync() -> List[str]: r = dns.resolver.Resolver() r.timeout = settings.DNS_TIMEOUT r.lifetime = settings.DNS_LIFETIME try: answers = r.resolve(domain, 'TXT') except Exception: return [] out = [] for rr in answers: s = ''.join(part.decode('utf-8') if isinstance(part, bytes) else str(part) for part in getattr(rr, 'strings', [])) if not s: s = str(rr) s = s.strip('"') out.append(s) return out return await asyncio.to_thread(_sync) def parse_spf(txt_records: List[str]) -> Dict[str, Any]: spf_records = [t for t in txt_records if t.lower().startswith('v=spf1')] details = [] for spf in spf_records: tokens = spf.split() mechanisms = [t for t in tokens[1:]] qualifier = next((t for t in tokens if t.endswith('all')), None) details.append({ 'record': spf, 'mechanisms': mechanisms, 'has_all': any(tok.endswith('all') for tok in tokens), 'all_qualifier': qualifier, }) return { 'present': len(spf_records) > 0, 'records': spf_records, 'parsed': details, } async def smtp_starttls_probe(host: str, port: int = 25) -> Dict[str, Any]: def _sync() -> Dict[str, Any]: context = ssl.create_default_context() context.check_hostname = True context.verify_mode = ssl.CERT_REQUIRED result: Dict[str, Any] = { 'host': host, 'port': port, 'ehlo': False, 'starttls_advertised': False, 'tls_established': False, 'tls_version': None, 'cipher': None, 'cert': None, } try: with smtplib.SMTP(host=host, port=port, timeout=settings.SMTP_TIMEOUT) as server: code, _ = server.ehlo() result['ehlo'] = (200 <= code < 400) if 'starttls' in (server.esmtp_features or {}): result['starttls_advertised'] = True code, _ = server.starttls(context=context) if 200 <= code < 400: result['tls_established'] = True server.ehlo() try: sslobj = server.sock result['tls_version'] = getattr(sslobj, 'version', lambda: None)() result['cipher'] = getattr(sslobj, 'cipher', lambda: None)() try: cert = sslobj.getpeercert() result['cert'] = cert except Exception: pass except Exception: pass else: result['starttls_advertised'] = False except (ssl.SSLError, smtplib.SMTPException, OSError, socket.error) as e: result['error'] = str(e) return result return await asyncio.to_thread(_sync) # Cert date parsing (OpenSSL style: 'Jun 5 12:00:00 2025 GMT') def parse_cert_time(s: str) -> datetime | None: try: norm = ' '.join(s.split()) return datetime.strptime(norm, '%b %d %H:%M:%S %Y %Z') except Exception: return None