152 lines
5.8 KiB
Python
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()
|