Files
Sales-and-Marketing/address_book/conference_mail_merge/webapp.py
T
beantownbabe b745d6f15d Unify contact sheet, add reconnect message template, and scale the contacts UI
conference_contacts.xlsx is now the single working file behind both the dash's
Download button and the mail-merge tool -- populated with all 139 SPIE 2026
presenters (not just ones with emails already found), so Peter can fill in
emails as he meets people at the conference. Also adds a second message
template (reconnect_message) alongside the existing wavegate_focus one, and
a search/scroll UI for the contacts list now that it's no longer a handful
of rows.
2026-08-16 09:48:12 +02:00

263 lines
10 KiB
Python

#!/usr/bin/env python3
"""
Local web app for the conference mail merge -- contacts with checkboxes,
a choice of standard message templates, and batch generate/send.
Run: python3 webapp.py
Then open http://localhost:8505 in a browser.
To enable the "Preview & Send Batch" -> actual sending step, set these
environment variables before starting the app:
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS
Without them, the app still works for generating .eml drafts -- sending
just stays disabled.
"""
import os
import subprocess
from datetime import datetime
from pathlib import Path
import pandas as pd
from flask import Flask, render_template, request
import mail_merge as mm
HERE = Path(__file__).parent
MESSAGE_TEMPLATES_DIR = HERE / "message_templates"
DEFAULT_ATTACHMENTS = [
Path.home() / "Documents" / "TXproducts_Materials" / "txservices.pdf",
Path.home() / "Documents" / "TXproducts_Materials" / "wavegate.pdf",
]
app = Flask(__name__)
def get_contacts():
if not mm.CONTACTS_FILE.exists():
return []
try:
df = pd.read_excel(mm.CONTACTS_FILE, sheet_name="Contacts")
except Exception:
return []
contacts = []
for _, row in df.iterrows():
conference = str(row.get("Conference", "")).strip()
conference = "" if conference.lower() == "nan" else conference
draft_opened = str(row.get("Draft Opened", "")).strip()
draft_opened = "" if draft_opened.lower() == "nan" else draft_opened
contacts.append({
"first": row.get("First Name", ""),
"last": row.get("Last Name", ""),
"email": row.get("Email", ""),
"conference": conference,
"sent": str(row.get("Materials Sent", "")).strip().lower() == "yes",
"draft_opened": draft_opened,
})
return contacts
def get_templates():
return sorted(p.name for p in MESSAGE_TEMPLATES_DIR.glob("*.txt"))
def get_attachment_options():
return [{"name": p.name, "path": str(p), "exists": p.exists()} for p in DEFAULT_ATTACHMENTS]
def rows_for_selection(selected_emails, template_name):
"""(row_dict, subject, body) for each selected contact, using the given template."""
df = pd.read_excel(mm.CONTACTS_FILE, sheet_name="Contacts")
wanted = {e.strip().lower() for e in selected_emails}
df = df[df["Email"].astype(str).str.strip().str.lower().isin(wanted)]
subject_template, body_template = mm.load_template(MESSAGE_TEMPLATES_DIR / template_name)
results = []
for _, row in df.iterrows():
row_dict = row.to_dict()
subject = mm.fill(subject_template, row_dict)
body = mm.fill(body_template, row_dict)
results.append((row_dict, subject, body))
return results
def smtp_configured():
return all(os.environ.get(k) for k in ("SMTP_HOST", "SMTP_USER", "SMTP_PASS"))
@app.route("/")
def index():
return render_template(
"index.html",
contacts=get_contacts(),
templates=get_templates(),
attachments=get_attachment_options(),
default_from=mm.DEFAULT_FROM,
error=request.args.get("error"),
preselect_email=(request.args.get("email") or "").strip().lower(),
)
@app.route("/generate", methods=["POST"])
def generate():
selected = request.form.getlist("contacts")
template_name = request.form.get("template")
attachments = request.form.getlist("attachments")
from_address = request.form.get("from_address") or mm.DEFAULT_FROM
if not selected:
return render_template("index.html", contacts=get_contacts(), templates=get_templates(),
attachments=get_attachment_options(), default_from=mm.DEFAULT_FROM,
error="Check at least one contact first.")
try:
full_df = pd.read_excel(mm.CONTACTS_FILE, sheet_name="Contacts")
except Exception as exc:
return render_template("index.html", contacts=get_contacts(), templates=get_templates(),
attachments=get_attachment_options(), default_from=mm.DEFAULT_FROM,
error=f"Close the Excel file and try again. ({exc})")
if "Draft File" not in full_df.columns:
full_df["Draft File"] = ""
try:
rows = rows_for_selection(selected, template_name)
except ValueError as exc:
return render_template("index.html", contacts=get_contacts(), templates=get_templates(),
attachments=get_attachment_options(), default_from=mm.DEFAULT_FROM,
error=str(exc))
lines = []
warnings = []
for row_dict, subject, body in rows:
if "[[MISSING:" in subject or "[[MISSING:" in body:
warnings.append(f"Missing field(s) for {row_dict['Email']} -- check before sending.")
msg = mm.build_message(subject, body, row_dict, attachments, from_address)
out_path = mm.write_draft(msg, row_dict)
lines.append(f"Wrote draft: {out_path}")
match = full_df["Email"].astype(str).str.strip().str.lower() == str(row_dict["Email"]).strip().lower()
full_df.loc[match, "Draft File"] = str(out_path)
full_df.to_excel(mm.CONTACTS_FILE, index=False, sheet_name="Contacts")
mm.add_draft_links(mm.CONTACTS_FILE)
return render_template("results.html", lines=lines, warnings=warnings)
@app.route("/open_in_mail", methods=["POST"])
def open_in_mail():
selected = request.form.getlist("contacts")
template_name = request.form.get("template")
attachments = request.form.getlist("attachments")
from_address = request.form.get("from_address") or mm.DEFAULT_FROM
if not selected:
return render_template("index.html", contacts=get_contacts(), templates=get_templates(),
attachments=get_attachment_options(), default_from=mm.DEFAULT_FROM,
error="Check at least one contact first.")
try:
full_df = pd.read_excel(mm.CONTACTS_FILE, sheet_name="Contacts")
except Exception as exc:
return render_template("index.html", contacts=get_contacts(), templates=get_templates(),
attachments=get_attachment_options(), default_from=mm.DEFAULT_FROM,
error=f"Close the Excel file and try again. ({exc})")
if "Draft Opened" not in full_df.columns:
full_df["Draft Opened"] = ""
try:
rows = rows_for_selection(selected, template_name)
except ValueError as exc:
return render_template("index.html", contacts=get_contacts(), templates=get_templates(),
attachments=get_attachment_options(), default_from=mm.DEFAULT_FROM,
error=str(exc))
lines = []
warnings = []
opened_any = False
for row_dict, subject, body in rows:
if "[[MISSING:" in subject or "[[MISSING:" in body:
warnings.append(f"Missing field(s) for {row_dict['Email']} -- check before sending.")
try:
mm.open_in_mail_app(subject, body, row_dict["Email"], attachments, from_address)
lines.append(f"Opened in Mail.app: {row_dict['First Name']} {row_dict['Last Name']} <{row_dict['Email']}>")
timestamp = datetime.now().strftime("%d.%m.%y %H:%M")
match = full_df["Email"].astype(str).str.strip().str.lower() == str(row_dict["Email"]).strip().lower()
full_df.loc[match, "Draft Opened"] = timestamp
opened_any = True
mm.log_to_dash_outreach(row_dict["Email"], f"Draft opened in Mail.app ({template_name}), {timestamp}")
except RuntimeError as exc:
warnings.append(f"Could not open Mail.app draft for {row_dict['Email']}: {exc}")
if opened_any:
full_df.to_excel(mm.CONTACTS_FILE, index=False, sheet_name="Contacts")
return render_template("results.html", lines=lines, warnings=warnings)
@app.route("/preview", methods=["POST"])
def preview():
selected = request.form.getlist("contacts")
template_name = request.form.get("template")
attachments = request.form.getlist("attachments")
from_address = request.form.get("from_address") or mm.DEFAULT_FROM
if not selected:
return render_template("index.html", contacts=get_contacts(), templates=get_templates(),
attachments=get_attachment_options(), default_from=mm.DEFAULT_FROM,
error="Check at least one contact first.")
try:
rows = rows_for_selection(selected, template_name)
except ValueError as exc:
return render_template("index.html", contacts=get_contacts(), templates=get_templates(),
attachments=get_attachment_options(), default_from=mm.DEFAULT_FROM,
error=str(exc))
table_rows = [{"email": r["Email"], "subject": s, "body": b} for r, s, b in rows]
return render_template(
"preview.html",
rows=table_rows,
smtp_ready=smtp_configured(),
contacts=selected,
template=template_name,
attachments=attachments,
from_address=from_address,
)
@app.route("/send", methods=["POST"])
def send():
if not smtp_configured():
return render_template("send_results.html", lines=["SMTP isn't configured -- nothing was sent."])
selected = request.form.getlist("contacts")
template_name = request.form.get("template")
attachments = request.form.getlist("attachments")
from_address = request.form.get("from_address") or mm.DEFAULT_FROM
rows = rows_for_selection(selected, template_name)
lines = []
for row_dict, subject, body in rows:
msg = mm.build_message(subject, body, row_dict, attachments, from_address)
try:
mm.send_message(msg)
lines.append(f"Sent to {row_dict['Email']}")
except Exception as exc:
lines.append(f"FAILED for {row_dict['Email']}: {exc}")
return render_template("send_results.html", lines=lines)
@app.route("/open_folder", methods=["POST"])
def open_folder():
mm.OUTPUT_DIR.mkdir(exist_ok=True)
subprocess.run(["open", str(mm.OUTPUT_DIR)])
return ("", 204)
if __name__ == "__main__":
app.run(port=8505, debug=False)