_styles.py: Centralized typography, spacing, and formatting for all 26 DOCX generators. Calibri 9.5pt body, 1.15 line spacing, navy headings, consistent signature blocks, page numbers, PW footer. All generators will be migrated to use this instead of defining their own styles. Campaign tools: - campaign_template.html: Styled email template for Listmonk campaigns - populate_deficiency_list.py: Populates Listmonk with FCC deficiency data - send_test_campaigns.py: Sends test emails with real carrier data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
121 lines
4.9 KiB
Python
121 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Send test compliance gap emails to justin@performancewest.net using real carrier data."""
|
|
import json
|
|
import smtplib
|
|
import subprocess
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.text import MIMEText
|
|
|
|
ISSUE_LABELS = {
|
|
"ss_partial_note": "STIR/SHAKEN partial implementation \u2014 upstream provider not named in filing",
|
|
"ss_vsp_no_shaken": "Voice Service Provider without STIR/SHAKEN implementation",
|
|
"conflicting_classification": "Unusual provider classification \u2014 may need correction",
|
|
"missing_kyc": "Missing Know Your Customer (KYC) procedures",
|
|
"missing_material_change": "Missing 10-business-day material change update commitment",
|
|
"missing_dno": "Missing Do-Not-Originate (DNO) list enforcement",
|
|
"missing_traceback": "Missing 24-hour traceback response commitment",
|
|
"missing_recertification": "Missing annual recertification acknowledgment",
|
|
"missing_perjury": "Missing perjury declaration",
|
|
"missing_mitigation": "Missing robocall mitigation program details",
|
|
"no_classification": "No provider classification selected",
|
|
"ss_intermediate_complete": "Intermediate provider claims Complete STIR/SHAKEN",
|
|
}
|
|
|
|
# Query real carrier data
|
|
result = subprocess.run([
|
|
"docker", "exec", "performancewest-api-postgres-1", "psql", "-U", "pw",
|
|
"performancewest", "-t", "-A", "-F", "|", "-c",
|
|
"""SELECT a.frn, a.business_name, a.total_deficiencies, a.severity,
|
|
r.contact_email, r.implementation, r.last_recertified::text,
|
|
a.structured_checks::text, a.pdf_checks::text
|
|
FROM fcc_rmd_audit_results a
|
|
JOIN fcc_rmd r ON r.frn = a.frn
|
|
WHERE a.total_deficiencies > 0
|
|
AND a.severity IN ('major', 'critical')
|
|
AND r.contact_email IS NOT NULL
|
|
AND r.removed_from_rmd = FALSE
|
|
ORDER BY a.total_deficiencies DESC
|
|
LIMIT 5"""
|
|
], capture_output=True, text=True)
|
|
|
|
with open("/tmp/campaign_template.html") as f:
|
|
template = f.read()
|
|
|
|
for line in result.stdout.strip().split("\n"):
|
|
if not line.strip():
|
|
continue
|
|
parts = line.split("|")
|
|
if len(parts) < 9:
|
|
continue
|
|
|
|
frn = parts[0].strip()
|
|
company = parts[1].strip()
|
|
deficiency_count = parts[2].strip()
|
|
severity = parts[3].strip()
|
|
email_orig = parts[4].strip()
|
|
implementation = parts[5].strip()
|
|
last_recert = parts[6].strip()
|
|
structured_json = parts[7].strip()
|
|
pdf_json = parts[8].strip()
|
|
|
|
# Merge both structured and PDF checks, prioritize major/critical
|
|
all_checks = []
|
|
for cj in [structured_json, pdf_json]:
|
|
try:
|
|
checks = json.loads(cj)
|
|
if isinstance(checks, list):
|
|
all_checks.extend(checks)
|
|
except Exception:
|
|
pass
|
|
|
|
# Sort: critical first, then major, then minor — skip minor for email
|
|
severity_order = {"critical": 0, "major": 1, "minor": 2}
|
|
all_checks.sort(key=lambda c: severity_order.get(c.get("severity", "minor"), 2))
|
|
|
|
items = []
|
|
seen_ids = set()
|
|
for c in all_checks:
|
|
cid = c.get("id", "")
|
|
if cid in seen_ids:
|
|
continue
|
|
seen_ids.add(cid)
|
|
sev = c.get("severity", "minor")
|
|
if sev == "minor":
|
|
continue # skip minor issues in email
|
|
label = ISSUE_LABELS.get(cid, c.get("label", cid))
|
|
items.append(f"<li>{label}</li>")
|
|
|
|
if not items:
|
|
# If all were minor, include them anyway
|
|
for c in all_checks:
|
|
cid = c.get("id", "")
|
|
label = ISSUE_LABELS.get(cid, c.get("label", cid))
|
|
items.append(f"<li>{label}</li>")
|
|
|
|
issues_html = '<ul style="margin:0;padding:0 0 0 16px">' + "".join(items[:6]) + "</ul>"
|
|
|
|
# Build the email from template
|
|
body = template
|
|
body = body.replace("{{ .Subscriber.Name }}", "there")
|
|
body = body.replace("{{ .Subscriber.Attribs.company }}", company)
|
|
body = body.replace("{{ .Subscriber.Attribs.fcc_frn }}", frn)
|
|
body = body.replace("{{ .Subscriber.Attribs.issues_html }}", issues_html)
|
|
body = body.replace("{{ .Subscriber.Attribs.implementation }}", implementation or "Unknown")
|
|
body = body.replace("{{ .Subscriber.Attribs.last_recertified }}", last_recert or "Unknown")
|
|
body = body.replace("{{ UnsubscribeURL }}", "#")
|
|
|
|
msg = MIMEMultipart("alternative")
|
|
msg["From"] = "Performance West <noreply@performancewest.net>"
|
|
msg["To"] = "justin@performancewest.net"
|
|
msg["Subject"] = f"[TEST] FCC Compliance Alert - {company}"
|
|
msg["Reply-To"] = "info@performancewest.net"
|
|
msg.attach(MIMEText(body, "html"))
|
|
|
|
with smtplib.SMTP("email-smtp.us-east-2.amazonaws.com", 587, timeout=30) as s:
|
|
s.starttls()
|
|
s.login("AKIAYEWLMNWPHSHQWCRD", "BKrUBud+KjyaRA1RiA26FFu1R+hqR4cpFShwbZf7RUzG")
|
|
s.send_message(msg)
|
|
|
|
print(f"Sent: {company} ({frn}) - {deficiency_count} issues (would go to {email_orig})")
|
|
|
|
print("\nDone - all test emails sent to justin@performancewest.net")
|