Files

152 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""BoxPilot Logistics email triage — detect and move solicitations.
Heuristic: if sender is from a free/personal email domain (gmail, yahoo, etc.)
AND the email body or subject references a different business domain,
it's likely solicitation. Move to INBOX.Solicitation.
"""
import imaplib, email, re, os, json, hashlib
from email.utils import parsedate_to_datetime
from datetime import datetime, timedelta, timezone
HOST = "mail.boxpilotlogistics.com"
PORT = 993
USER = "hello@boxpilotlogistics.com"
PW = "1New.opportunity"
SOLICIT_FOLDER = "Solicitation"
CHECK_HOURS = 48
# Free/personal email domains — if sender is from these AND claims another company, it's sus
FREE_DOMAINS = {
"gmail.com", "yahoo.com", "yahoo.co.uk", "ymail.com", "outlook.com",
"hotmail.com", "live.com", "msn.com", "aol.com", "icloud.com",
"me.com", "mac.com", "protonmail.com", "proton.me", "pm.me",
"mail.com", "inbox.com", "zoho.com", "yandex.com", "gmx.com",
"fastmail.com", "tutanota.com", "rediffmail.com", "libero.it",
"web.de", "gmx.de", "t-online.de", "online.de",
}
def safe_str(val):
if val is None: return ""
if isinstance(val, str): return val
try: return str(val)
except: return ""
def get_body_text(msg):
"""Extract plain text body."""
body = ""
if msg.is_multipart():
for part in msg.walk():
ct = part.get_content_type()
if ct == "text/plain":
payload = part.get_payload(decode=True)
if payload:
body += payload.decode("utf-8", errors="replace")
elif ct == "text/html" and not body:
payload = part.get_payload(decode=True)
if payload:
body += payload.decode("utf-8", errors="replace")
else:
payload = msg.get_payload(decode=True)
if payload:
body = payload.decode("utf-8", errors="replace")
return body
def extract_domains(text):
"""Extract all domain names from text (from URLs and email addresses)."""
domains = set()
# URLs
for m in re.finditer(r'https?://(?:www\.)?([a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.[a-z]{2,}(?:\.[a-z]{2,})?)', text, re.IGNORECASE):
d = m.group(1).lower()
# Skip common non-business domains
if d not in ('google.com', 'youtube.com', 'facebook.com', 'linkedin.com', 'twitter.com', 'x.com', 'instagram.com'):
domains.add(d)
# Email addresses in body
for m in re.finditer(r'[a-z0-9._%+-]+@([a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.[a-z]{2,}(?:\.[a-z]{2,})?)', text, re.IGNORECASE):
d = m.group(1).lower()
if d not in FREE_DOMAINS:
domains.add(d)
return domains
def is_solicitation(msg):
"""Determine if a message is likely solicitation."""
sender = safe_str(msg.get("From", "")).lower()
subject = safe_str(msg.get("Subject", "")).lower()
body = get_body_text(msg).lower()
full_text = subject + " " + body
# Extract sender's domain
sender_domain = ""
m = re.search(r'@([a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.[a-z]{2,}(?:\.[a-z]{2,})?)', sender)
if m:
sender_domain = m.group(1).lower()
# If sender is from a free domain, check if they claim another company
if sender_domain in FREE_DOMAINS:
# Extract business domains mentioned in the email
biz_domains = extract_domains(full_text)
# If they reference a business domain different from their sender domain
# AND that business domain is a real company domain (not another free domain)
legit_biz = [d for d in biz_domains if d not in FREE_DOMAINS]
if legit_biz:
return True, f"Free domain ({sender_domain}) referencing business domain(s): {', '.join(list(legit_biz)[:3])}"
# Also flag common solicitation subject patterns from free domains
if sender_domain in FREE_DOMAINS:
spam_kw = ["quote", "insurance", "factoring", "fuel", "dispatch", "load board",
"freight", "logistics", "carrier", "trucking", "broker", "authority",
"mc authority", "dot authority", "compliance", "ifta", "irp",
"elog", "eld", "safety", "audit", "lease", "warranty", "extended warranty"]
found = [kw for kw in spam_kw if kw in subject]
if found:
return True, f"Free domain ({sender_domain}) with solicitation subject keywords: {', '.join(found)}"
return False, ""
def main():
try:
conn = imaplib.IMAP4_SSL(HOST, PORT)
conn.login(USER, PW)
except Exception as e:
print(f"ERROR: Connection failed: {e}")
return
# Select inbox
conn.select("INBOX")
# Search for recent unread messages
since = (datetime.now(timezone.utc) - timedelta(hours=CHECK_HOURS)).strftime("%d-%b-%Y")
status, data = conn.search(None, f'(UNSEEN SINCE {since})')
if status != "OK" or not data[0]:
conn.logout()
return # silent
moved = 0
for num in data[0].split():
status, msg_data = conn.fetch(num, "(RFC822)")
if status != "OK":
continue
raw = email.message_from_bytes(msg_data[0][1])
result, reason = is_solicitation(raw)
if result:
# Copy to Solicitation folder, then delete from inbox
copy_status = conn.copy(num, SOLICIT_FOLDER)
if copy_status[0] == "OK":
conn.store(num, "+FLAGS", "\\Deleted")
moved += 1
subj = safe_str(raw.get("Subject", "(no subject)"))
sender = safe_str(raw.get("From", "(unknown)"))
# Expunge deleted messages
if moved > 0:
conn.expunge()
conn.logout()
if moved > 0:
print(f"📬 BoxPilot — Moved {moved} solicitation(s) to Solicitation folder")
# silent if nothing moved
if __name__ == "__main__":
main()