Add web UI, sender profiles, purge rules, and infosec action
Extends the pipeline with infosec classification, sender profile tracking, and configurable purge rules. Adds a web dashboard for managing rules and monitoring email processing. Includes new migrations and seed script. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3bfda9e585
commit
ecc6681432
48 changed files with 4394 additions and 59 deletions
170
seed_from_pulse.py
Normal file
170
seed_from_pulse.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Seed howl database from pulse_autotask (Autotask contacts/companies).
|
||||
|
||||
Sources:
|
||||
customers + customer_emails <- company_type=1 (Customer) active contacts w/ emails
|
||||
vendors + vendor_emails <- company_type=7 (Partner) active contacts w/ emails
|
||||
whitelist <- unique domains from all client company email addresses
|
||||
"""
|
||||
|
||||
import psycopg2
|
||||
import re
|
||||
|
||||
PULSE_PARAMS = dict(host='localhost', port=5432, user='pulse_user',
|
||||
password='9KuYTjjGEB7NsJc_togj6R9wYLRRrhudZiR4@i@N',
|
||||
dbname='pulse_autotask')
|
||||
HOWL_PARAMS = dict(host='localhost', port=5434, user='howl',
|
||||
password='ztPEGUexRYkfWqUtd15WMdJF98N1jsui',
|
||||
dbname='howl')
|
||||
|
||||
EMAIL_RE = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]+$')
|
||||
|
||||
def valid_email(e):
|
||||
return e and EMAIL_RE.match(e.strip())
|
||||
|
||||
def domain_of(e):
|
||||
return e.strip().lower().split('@')[1] if e and '@' in e else None
|
||||
|
||||
def seed_contacts(pulse_cur, howl_cur, company_type, parent_table, email_table, fk_col):
|
||||
"""Generic: pull contacts of given company_type, insert parent rows then email rows."""
|
||||
pulse_cur.execute("""
|
||||
SELECT
|
||||
trim(concat(c.first_name, ' ', c.last_name)) AS name,
|
||||
co.company_name AS company,
|
||||
COALESCE(NULLIF(c.phone,''), NULLIF(co.phone,'')) AS phone,
|
||||
c.email_address,
|
||||
c.email_address2,
|
||||
c.email_address3,
|
||||
c.primary_contact
|
||||
FROM contacts c
|
||||
JOIN companies co ON c.company_id = co.id
|
||||
WHERE co.company_type = %s
|
||||
AND co.is_active = true AND co.is_deleted = false
|
||||
AND c.is_active = true AND c.is_deleted = false
|
||||
AND c.email_address IS NOT NULL AND c.email_address != ''
|
||||
ORDER BY co.company_name, c.primary_contact DESC, c.last_name, c.first_name
|
||||
""", (company_type,))
|
||||
rows = pulse_cur.fetchall()
|
||||
|
||||
inserted_parents = 0
|
||||
inserted_emails = 0
|
||||
skipped_emails = 0
|
||||
seen_emails = set() # guard against dupes within this batch
|
||||
|
||||
for row in rows:
|
||||
name, company, phone, e1, e2, e3, is_primary = row
|
||||
|
||||
# Collect valid emails for this contact
|
||||
emails = []
|
||||
for addr in [e1, e2, e3]:
|
||||
if valid_email(addr):
|
||||
emails.append(addr.strip().lower())
|
||||
if not emails:
|
||||
continue
|
||||
|
||||
# Insert parent row
|
||||
howl_cur.execute(
|
||||
f"INSERT INTO {parent_table} (name, company, phone) VALUES (%s,%s,%s) RETURNING id",
|
||||
(name or 'Unknown', company, phone)
|
||||
)
|
||||
parent_id = howl_cur.fetchone()[0]
|
||||
inserted_parents += 1
|
||||
|
||||
# Insert email rows
|
||||
for i, addr in enumerate(emails):
|
||||
if addr in seen_emails:
|
||||
skipped_emails += 1
|
||||
continue
|
||||
try:
|
||||
howl_cur.execute(
|
||||
f"INSERT INTO {email_table} ({fk_col}, email_address, label, is_primary) VALUES (%s,%s,%s,%s)",
|
||||
(parent_id, addr, 'work', i == 0 and is_primary)
|
||||
)
|
||||
seen_emails.add(addr)
|
||||
inserted_emails += 1
|
||||
except psycopg2.errors.UniqueViolation:
|
||||
howl_cur.connection.rollback()
|
||||
skipped_emails += 1
|
||||
except Exception as e:
|
||||
howl_cur.connection.rollback()
|
||||
print(f" WARN email {addr}: {e}")
|
||||
skipped_emails += 1
|
||||
|
||||
return inserted_parents, inserted_emails, skipped_emails
|
||||
|
||||
|
||||
def seed_whitelist(pulse_cur, howl_cur):
|
||||
"""Insert one whitelist row per unique domain used by customer company contacts."""
|
||||
pulse_cur.execute("""
|
||||
SELECT DISTINCT lower(split_part(c.email_address, '@', 2)) AS domain
|
||||
FROM contacts c
|
||||
JOIN companies co ON c.company_id = co.id
|
||||
WHERE co.company_type = 1
|
||||
AND co.is_active = true AND co.is_deleted = false
|
||||
AND c.is_active = true AND c.is_deleted = false
|
||||
AND c.email_address LIKE '%@%'
|
||||
ORDER BY 1
|
||||
""")
|
||||
domains = [r[0] for r in pulse_cur.fetchall() if r[0] and '.' in r[0]]
|
||||
|
||||
inserted = 0
|
||||
for domain in domains:
|
||||
try:
|
||||
howl_cur.execute(
|
||||
"INSERT INTO whitelist (domain, description, added_by) VALUES (%s,%s,%s)",
|
||||
(domain, 'Auto-seeded from Autotask customer contacts', 'seed_from_pulse.py')
|
||||
)
|
||||
inserted += 1
|
||||
except psycopg2.errors.UniqueViolation:
|
||||
howl_cur.connection.rollback()
|
||||
except Exception as e:
|
||||
howl_cur.connection.rollback()
|
||||
print(f" WARN domain {domain}: {e}")
|
||||
|
||||
return inserted
|
||||
|
||||
|
||||
def main():
|
||||
print("Connecting to pulse_autotask …")
|
||||
pulse = psycopg2.connect(**PULSE_PARAMS)
|
||||
pulse.autocommit = False
|
||||
pc = pulse.cursor()
|
||||
|
||||
print("Connecting to howl …")
|
||||
howl = psycopg2.connect(**HOWL_PARAMS)
|
||||
howl.autocommit = False
|
||||
hc = howl.cursor()
|
||||
|
||||
try:
|
||||
# --- Customers (company_type=1) ---
|
||||
print("\nSeeding customers …")
|
||||
cp, ce, cs = seed_contacts(pc, hc, 1, 'customers', 'customer_emails', 'customer_id')
|
||||
print(f" customers: {cp:4d} rows")
|
||||
print(f" customer_emails: {ce:4d} rows ({cs} skipped/dupes)")
|
||||
|
||||
# --- Vendors / Partners (company_type=7) ---
|
||||
print("\nSeeding vendors …")
|
||||
vp, ve, vs = seed_contacts(pc, hc, 7, 'vendors', 'vendor_emails', 'vendor_id')
|
||||
print(f" vendors: {vp:4d} rows")
|
||||
print(f" vendor_emails: {ve:4d} rows ({vs} skipped/dupes)")
|
||||
|
||||
# --- Whitelist domains ---
|
||||
print("\nSeeding whitelist …")
|
||||
wl = seed_whitelist(pc, hc)
|
||||
print(f" whitelist: {wl:4d} domain rows")
|
||||
|
||||
howl.commit()
|
||||
print("\nDone — all changes committed.")
|
||||
|
||||
except Exception as e:
|
||||
howl.rollback()
|
||||
print(f"\nERROR — rolled back: {e}")
|
||||
raise
|
||||
finally:
|
||||
pc.close(); pulse.close()
|
||||
hc.close(); howl.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue