Initial Source code version

This commit is contained in:
Frank Agerholm 2025-12-11 12:07:17 +01:00
commit d979374cbc
23 changed files with 2628 additions and 0 deletions

116
app/workers/utils.py Normal file
View file

@ -0,0 +1,116 @@
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