task-queue-system/app/workers/utils.py

116 lines
No EOL
4.1 KiB
Python

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