Upload files to "/"

This commit is contained in:
2026-08-15 13:57:28 -04:00
commit c87f564e93
2 changed files with 1284 additions and 0 deletions
+833
View File
@@ -0,0 +1,833 @@
import sys
import os
import json
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime, timedelta
import threading
DB_FILE = "contacts_db.json"
DEFAULT_DB = {
"smtp_settings": {
"server": "smtp.gmail.com",
"port": 587,
"email": "",
"password": ""
},
"contacts": [
{
"callsign": "Glenn",
"name": "Glenn",
"email": "[email protected]",
"phone_gateway": "[email protected]",
"tags": "ARES, RACES, LEADER",
"receive_email": True,
"receive_text": True
}
]
}
class DispatchApp:
def __init__(self, root):
self.root = root
self.root.title("CCAR Alert Portal")
self.root.geometry("900x700")
self.all_individuals = []
self.active_schedulers = {}
self.task_counter = 0
self.ignore_trace = False
self.load_database()
self.create_widgets()
self.refresh_contact_list()
self.update_dispatch_selectors()
# --- RIGHT-CLICK CONTEXT MENU HELPER ---
def add_context_menu(self, widget):
menu = tk.Menu(widget, tearoff=0)
def cut_text():
widget.event_generate("<<Cut>>")
def copy_text():
widget.event_generate("<<Copy>>")
def paste_text():
widget.event_generate("<<Paste>>")
def select_all_text():
if isinstance(widget, tk.Text):
widget.tag_add("sel", "1.0", "end")
elif isinstance(widget, (ttk.Entry, tk.Entry)):
widget.select_range(0, tk.END)
widget.icursor(tk.END)
def clear_text():
if isinstance(widget, tk.Text):
widget.delete("1.0", tk.END)
elif isinstance(widget, (ttk.Entry, tk.Entry)):
widget.delete(0, tk.END)
menu.add_command(label="Cut", command=cut_text)
menu.add_command(label="Copy", command=copy_text)
menu.add_command(label="Paste", command=paste_text)
menu.add_separator()
menu.add_command(label="Select All", command=select_all_text)
menu.add_command(label="Clear", command=clear_text)
def show_popup(event):
menu.tk_popup(event.x_root, event.y_root)
# Windows and Linux Right-Click
widget.bind("<Button-3>", show_popup)
# macOS Right-Click / Ctrl-Click
widget.bind("<Button-2>", show_popup)
widget.bind("<Control-Button-1>", show_popup)
def load_database(self):
if not os.path.exists(DB_FILE):
with open(DB_FILE, 'w') as f:
json.dump(DEFAULT_DB, f, indent=4)
try:
with open(DB_FILE, 'r') as f:
self.db = json.load(f)
migrated = False
for c in self.db.get("contacts", []):
if "group" in c and "tags" not in c:
c["tags"] = f"ARES, {c['group'].strip().upper()}" if c["group"].strip().upper() != "ARES" else "ARES"
del c["group"]
migrated = True
if migrated:
self.save_database()
except Exception as e:
messagebox.showerror("Error", f"Failed to load database: {e}")
self.db = DEFAULT_DB
def save_database(self):
try:
with open(DB_FILE, 'w') as f:
json.dump(self.db, f, indent=4)
except Exception as e:
messagebox.showerror("Error", f"Failed to save database: {e}")
def create_widgets(self):
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill='both', expand=True, padx=10, pady=10)
self.tab_dispatch = ttk.Frame(self.notebook)
self.notebook.add(self.tab_dispatch, text="Send Dispatch")
self.tab_roster = ttk.Frame(self.notebook)
self.notebook.add(self.tab_roster, text="Manage Roster")
self.tab_config = ttk.Frame(self.notebook)
self.notebook.add(self.tab_config, text="SMTP Setup")
self.setup_dispatch_tab()
self.setup_roster_tab()
self.setup_config_tab()
# --- TAB 1: DISPATCH ---
def setup_dispatch_tab(self):
top_frame = ttk.Frame(self.tab_dispatch)
top_frame.pack(fill='x', padx=10, pady=10)
# Targeting Frame
target_frame = ttk.LabelFrame(top_frame, text=" Recipient Targeting Mode ", padding=10)
target_frame.pack(side='left', fill='both', expand=True, padx=(0, 5))
self.target_mode = tk.StringVar(value="all")
rb_all = ttk.Radiobutton(target_frame, text="All Roster Members", variable=self.target_mode, value="all", command=self.toggle_target_fields)
rb_all.grid(row=0, column=0, sticky='w', padx=5, pady=5)
rb_group = ttk.Radiobutton(target_frame, text="By Group Tag Filter:", variable=self.target_mode, value="group", command=self.toggle_target_fields)
rb_group.grid(row=1, column=0, sticky='w', padx=5, pady=5)
self.cb_groups = ttk.Combobox(target_frame, state="disabled", width=15)
self.cb_groups.grid(row=1, column=1, sticky='w', padx=5, pady=5)
rb_indiv = ttk.Radiobutton(target_frame, text="Individual Operator:", variable=self.target_mode, value="individual", command=self.toggle_target_fields)
rb_indiv.grid(row=2, column=0, sticky='w', padx=5, pady=5)
self.indiv_search_var = tk.StringVar()
self.indiv_search_var.trace_add("write", self.on_individual_type)
self.cb_individuals = ttk.Combobox(target_frame, state="disabled", width=35, textvariable=self.indiv_search_var)
self.cb_individuals.grid(row=2, column=1, sticky='w', padx=5, pady=5)
self.add_context_menu(self.cb_individuals)
# Transmission Channel & Dynamic Sender Options
channel_frame = ttk.LabelFrame(top_frame, text=" Transmission Settings ", padding=10)
channel_frame.pack(side='right', fill='both', padx=(5, 0))
self.dispatch_send_email = tk.BooleanVar(value=True)
self.dispatch_send_text = tk.BooleanVar(value=False)
chk_send_email = ttk.Checkbutton(channel_frame, text="Send via Email", variable=self.dispatch_send_email)
chk_send_email.pack(anchor='w', padx=10, pady=2)
chk_send_text = ttk.Checkbutton(channel_frame, text="Send via Text (SMS)", variable=self.dispatch_send_text)
chk_send_text.pack(anchor='w', padx=10, pady=2)
# Custom Sender Box
ttk.Label(channel_frame, text="Display Sender Email:").pack(anchor='w', padx=10, pady=(10, 2))
self.ent_custom_sender = ttk.Entry(channel_frame, width=28)
self.ent_custom_sender.pack(anchor='w', padx=10, pady=2)
cfg = self.db.get("smtp_settings", {})
self.ent_custom_sender.insert(0, cfg.get("email", ""))
self.add_context_menu(self.ent_custom_sender)
# Scheduler Panel
sched_frame = ttk.LabelFrame(self.tab_dispatch, text=" Transmission Timing Scheduling ", padding=10)
sched_frame.pack(fill='x', padx=10, pady=5)
self.sched_choice = tk.StringVar(value="now")
rb_now = ttk.Radiobutton(sched_frame, text="Send Immediately", variable=self.sched_choice, value="now", command=self.toggle_sched_fields)
rb_now.grid(row=0, column=0, sticky='w', padx=5, pady=5)
rb_later = ttk.Radiobutton(sched_frame, text="Schedule Deployment:", variable=self.sched_choice, value="later", command=self.toggle_sched_fields)
rb_later.grid(row=0, column=1, sticky='w', padx=15, pady=5)
ttk.Label(sched_frame, text="Date (MM-DD-YY):").grid(row=0, column=2, sticky='w', padx=5)
self.ent_sched_date = ttk.Entry(sched_frame, width=12, state="disabled")
self.ent_sched_date.insert(0, datetime.now().strftime("%m-%d-%y"))
self.ent_sched_date.grid(row=0, column=3, sticky='w', padx=5)
self.add_context_menu(self.ent_sched_date)
ttk.Label(sched_frame, text="Time (HH:MM 24hr):").grid(row=0, column=4, sticky='w', padx=5)
self.ent_sched_time = ttk.Entry(sched_frame, width=8, state="disabled")
self.ent_sched_time.insert(0, (datetime.now() + timedelta(hours=1)).strftime("%H:%M"))
self.ent_sched_time.grid(row=0, column=5, sticky='w', padx=5)
self.add_context_menu(self.ent_sched_time)
# Queue Status Trackers
self.lbl_queue_status = ttk.Label(sched_frame, text="Pending Alerts: 0", font=('Helvetica', 9, 'italic'), foreground='blue')
self.lbl_queue_status.grid(row=0, column=6, sticky='e', padx=(20, 5))
btn_manage_queue = ttk.Button(sched_frame, text="Manage Queue", command=self.open_queue_manager)
btn_manage_queue.grid(row=0, column=7, sticky='e', padx=5)
# Message Entry Layout
lbl_msg = ttk.Label(self.tab_dispatch, text="Emergency Alert Message Content (HTML Supported):", font=('Helvetica', 10, 'bold'))
lbl_msg.pack(anchor='w', padx=10, pady=5)
self.txt_message = tk.Text(self.tab_dispatch, height=8, wrap='word', undo=True)
self.txt_message.pack(fill='both', expand=True, padx=10, pady=5)
# Standard Keyboard Shortcuts
self.txt_message.bind("<Control-a>", self.handle_select_all)
self.txt_message.bind("<Control-c>", self.handle_copy)
self.txt_message.bind("<Control-v>", self.handle_paste)
self.txt_message.bind("<Control-x>", self.handle_cut)
# Attach Right-Click Context Menu
self.add_context_menu(self.txt_message)
# Action Buttons Frame
btn_frame = ttk.Frame(self.tab_dispatch)
btn_frame.pack(fill='x', padx=10, pady=10)
btn_send = ttk.Button(btn_frame, text="TRANSMIT DISPATCH", command=self.process_dispatch_request)
btn_send.pack(side='right', padx=5)
btn_clear_screen = ttk.Button(btn_frame, text="Clear Screen", command=self.clear_message_screen)
btn_clear_screen.pack(side='right', padx=5)
def handle_select_all(self, event):
self.txt_message.tag_add("sel", "1.0", "end")
return "break"
def handle_copy(self, event):
self.txt_message.event_generate("<<Copy>>")
return "break"
def handle_paste(self, event):
self.txt_message.event_generate("<<Paste>>")
return "break"
def handle_cut(self, event):
self.txt_message.event_generate("<<Cut>>")
return "break"
def clear_message_screen(self):
self.txt_message.delete('1.0', tk.END)
def toggle_target_fields(self):
mode = self.target_mode.get()
if mode == "all":
self.cb_individuals.config(state="disabled")
self.cb_groups.config(state="disabled")
elif mode == "group":
self.cb_groups.config(state="readonly")
self.cb_individuals.config(state="disabled")
elif mode == "individual":
self.cb_groups.config(state="disabled")
self.cb_individuals.config(state="normal")
self.ignore_trace = True
self.cb_individuals.set("")
self.ignore_trace = False
self.cb_individuals.focus_set()
def toggle_sched_fields(self):
if self.sched_choice.get() == "now":
self.ent_sched_date.config(state="disabled")
self.ent_sched_time.config(state="disabled")
else:
self.ent_sched_date.config(state="normal")
self.ent_sched_time.config(state="normal")
def update_dispatch_selectors(self):
unique_tags = set()
self.all_individuals = []
for c in self.db.get("contacts", []):
if c.get("tags"):
parts = [t.strip().upper() for t in c["tags"].split(",") if t.strip()]
unique_tags.update(parts)
display_name = f"{c.get('callsign', 'VOLUNTEER')} - {c.get('name', '')} ({c.get('email', '')})"
self.all_individuals.append(display_name)
self.cb_groups['values'] = sorted(list(unique_tags))
if unique_tags:
if "ARES" in unique_tags:
self.cb_groups.set("ARES")
else:
self.cb_groups.current(0)
self.all_individuals = sorted(self.all_individuals)
self.cb_individuals['values'] = self.all_individuals
def on_individual_type(self, *args):
if self.ignore_trace or self.target_mode.get() != "individual":
return
typed_text = self.cb_individuals.get()
typed_text_lower = typed_text.strip().lower()
if typed_text_lower == "":
filtered_values = self.all_individuals
else:
filtered_values = [person for person in self.all_individuals if typed_text_lower in person.lower()]
self.ignore_trace = True
self.cb_individuals['values'] = filtered_values
self.ignore_trace = False
# --- TAB 2: ROSTER MANAGEMENT ---
def setup_roster_tab(self):
top_panel = ttk.Frame(self.tab_roster)
top_panel.pack(fill='x', padx=10, pady=5)
btn_import = ttk.Button(top_panel, text="Import Google CSV", command=self.import_google_csv)
btn_import.pack(side='left', padx=5)
ttk.Label(top_panel, text="Search Roster:").pack(side='left', padx=(20, 5))
self.ent_search = ttk.Entry(top_panel, width=25)
self.ent_search.pack(side='left', padx=5)
self.ent_search.bind("<KeyRelease>", lambda event: self.refresh_contact_list())
self.add_context_menu(self.ent_search)
main_roster_frame = ttk.Frame(self.tab_roster)
main_roster_frame.pack(fill='both', expand=True, padx=10, pady=5)
self.tree = ttk.Treeview(main_roster_frame, columns=("Callsign", "Name", "Tags", "Email", "SMS Gateway", "Email Ok", "Text Ok"), show='headings')
self.tree.heading("Callsign", text="Callsign / ID")
self.tree.heading("Name", text="Name")
self.tree.heading("Tags", text="Assigned Tags / Groups")
self.tree.heading("Email", text="Email")
self.tree.heading("SMS Gateway", text="SMS Gateway Address")
self.tree.heading("Email Ok", text="Email Ok")
self.tree.heading("Text Ok", text="Text Ok")
self.tree.column("Email Ok", width=70, anchor='center')
self.tree.column("Text Ok", width=70, anchor='center')
self.tree.column("Tags", width=150)
self.tree.pack(side='left', fill='both', expand=True)
self.tree.bind("<<TreeviewSelect>>", self.on_contact_select)
editor_frame = ttk.LabelFrame(main_roster_frame, text=" Operator Profiles / Details ", padding=10)
editor_frame.pack(side='right', fill='y', padx=(10, 0))
ttk.Label(editor_frame, text="Callsign/ID:").grid(row=0, column=0, sticky='w', pady=2)
self.form_callsign = ttk.Entry(editor_frame, width=22)
self.form_callsign.grid(row=0, column=1, pady=2, sticky='w')
self.add_context_menu(self.form_callsign)
ttk.Label(editor_frame, text="Name:").grid(row=1, column=0, sticky='w', pady=2)
self.form_name = ttk.Entry(editor_frame, width=22)
self.form_name.grid(row=1, column=1, pady=2, sticky='w')
self.add_context_menu(self.form_name)
ttk.Label(editor_frame, text="Group Tags:").grid(row=2, column=0, sticky='w', pady=2)
self.form_tags = ttk.Entry(editor_frame, width=22)
self.form_tags.grid(row=2, column=1, pady=2, sticky='w')
self.add_context_menu(self.form_tags)
ttk.Label(editor_frame, text="(Separate with commas)", font=('Helvetica', 8, 'italic'), foreground='gray').grid(row=3, column=1, sticky='w')
ttk.Label(editor_frame, text="Email:").grid(row=4, column=0, sticky='w', pady=2)
self.form_email = ttk.Entry(editor_frame, width=22)
self.form_email.grid(row=4, column=1, pady=2, sticky='w')
self.add_context_menu(self.form_email)
ttk.Label(editor_frame, text="SMS Gateway:").grid(row=5, column=0, sticky='w', pady=2)
self.form_sms = ttk.Entry(editor_frame, width=22)
self.form_sms.grid(row=5, column=1, pady=2, sticky='w')
self.add_context_menu(self.form_sms)
ttk.Label(editor_frame, text="Channels:").grid(row=6, column=0, sticky='nw', pady=5)
chan_subframe = ttk.Frame(editor_frame)
chan_subframe.grid(row=6, column=1, sticky='w', pady=5)
self.form_receive_email = tk.BooleanVar(value=True)
self.form_receive_text = tk.BooleanVar(value=True)
ttk.Checkbutton(chan_subframe, text="Email Enabled", variable=self.form_receive_email).pack(anchor='w')
ttk.Checkbutton(chan_subframe, text="Text Enabled", variable=self.form_receive_text).pack(anchor='w')
btn_form_frame = ttk.Frame(editor_frame)
btn_form_frame.grid(row=7, column=0, columnspan=2, pady=15)
btn_save_user = ttk.Button(btn_form_frame, text="Save / Update", command=self.save_or_update_contact)
btn_save_user.pack(side='left', padx=2)
btn_del_user = ttk.Button(btn_form_frame, text="Delete Entry", command=self.delete_contact)
btn_del_user.pack(side='left', padx=2)
btn_clear_user = ttk.Button(btn_form_frame, text="Clear Form", command=self.clear_form)
btn_clear_user.pack(side='left', padx=2)
btn_wipe_all = ttk.Button(btn_form_frame, text="Wipe Roster", command=self.wipe_entire_roster)
btn_wipe_all.pack(side='left', padx=2)
self.selected_contact_email = None
self.clear_form()
def on_contact_select(self, event):
selected = self.tree.selection()
if not selected:
return
values = self.tree.item(selected[0], "values")
self.clear_form()
self.form_callsign.insert(0, values[0])
self.form_name.insert(0, values[1])
self.form_tags.insert(0, values[2])
self.form_email.insert(0, values[3])
self.form_sms.insert(0, values[4])
self.form_receive_email.set(True if values[5] == "YES" else False)
self.form_receive_text.set(True if values[6] == "YES" else False)
self.selected_contact_email = values[3]
def clear_form(self):
self.form_callsign.delete(0, tk.END)
self.form_name.delete(0, tk.END)
self.form_tags.delete(0, tk.END)
self.form_email.delete(0, tk.END)
self.form_sms.delete(0, tk.END)
self.form_receive_email.set(True)
self.form_receive_text.set(True)
self.selected_contact_email = None
def save_or_update_contact(self):
email = self.form_email.get().strip()
if not email:
messagebox.showwarning("Validation Error", "An email address is required.")
return
raw_tags = self.form_tags.get().strip()
tag_list = [t.strip().upper() for t in raw_tags.split(",") if t.strip()]
sanitized_tags_str = ", ".join(tag_list)
new_data = {
"callsign": self.form_callsign.get().strip(),
"name": self.form_name.get().strip(),
"tags": sanitized_tags_str,
"email": email,
"phone_gateway": self.form_sms.get().strip(),
"receive_email": self.form_receive_email.get(),
"receive_text": self.form_receive_text.get()
}
updated = False
for i, c in enumerate(self.db["contacts"]):
if (self.selected_contact_email and c.get("email") == self.selected_contact_email) or (not self.selected_contact_email and c.get("email") == email):
self.db["contacts"][i] = new_data
updated = True
break
if not updated:
self.db["contacts"].append(new_data)
self.save_database()
self.refresh_contact_list()
self.update_dispatch_selectors()
self.clear_form()
messagebox.showinfo("Success", "Roster record saved successfully.")
def delete_contact(self):
if not self.selected_contact_email:
messagebox.showwarning("Selection Required", "Select an operator entry from the roster list to delete.")
return
if messagebox.askyesno("Confirm Deletion", "Are you sure you want to completely remove this operator entry?"):
self.db["contacts"] = [c for c in self.db["contacts"] if c.get("email") != self.selected_contact_email]
self.save_database()
self.refresh_contact_list()
self.update_dispatch_selectors()
self.clear_form()
def wipe_entire_roster(self):
if messagebox.askyesno("CRITICAL WARNING", "Are you sure you want to permanently delete the ENTIRE contact roster?\n\nThis cannot be undone."):
if messagebox.askyesno("FINAL CONFIRMATION", "Confirming one last time: Erase all operator profiles?"):
self.db["contacts"] = []
self.save_database()
self.refresh_contact_list()
self.update_dispatch_selectors()
self.clear_form()
messagebox.showinfo("Roster Purged", "The entire contact database has been wiped clean.")
# --- TAB 3: CONFIGURATION ---
def setup_config_tab(self):
cfg = self.db.get("smtp_settings", {})
frame = ttk.Frame(self.tab_config, padding=20)
frame.pack(fill='both', expand=True)
ttk.Label(frame, text="SMTP Server:").grid(row=0, column=0, sticky='w', pady=5)
self.ent_server = ttk.Entry(frame, width=30)
self.ent_server.insert(0, cfg.get("server", "smtp.gmail.com"))
self.ent_server.grid(row=0, column=1, sticky='w', pady=5)
self.add_context_menu(self.ent_server)
ttk.Label(frame, text="Port:").grid(row=1, column=0, sticky='w', pady=5)
self.ent_port = ttk.Entry(frame, width=10)
self.ent_port.insert(0, str(cfg.get("port", 587)))
self.ent_port.grid(row=1, column=1, sticky='w', pady=5)
self.add_context_menu(self.ent_port)
ttk.Label(frame, text="Sender Email:").grid(row=2, column=0, sticky='w', pady=5)
self.ent_email = ttk.Entry(frame, width=30)
self.ent_email.insert(0, cfg.get("email", ""))
self.ent_email.grid(row=2, column=1, sticky='w', pady=5)
self.add_context_menu(self.ent_email)
ttk.Label(frame, text="App Password:").grid(row=3, column=0, sticky='w', pady=5)
self.ent_pass = ttk.Entry(frame, width=30, show="*")
self.ent_pass.insert(0, cfg.get("password", ""))
self.ent_pass.grid(row=3, column=1, sticky='w', pady=5)
self.add_context_menu(self.ent_pass)
btn_save_cfg = ttk.Button(frame, text="Save Settings", command=self.save_config)
btn_save_cfg.grid(row=4, column=1, sticky='e', pady=15)
def save_config(self):
self.db["smtp_settings"] = {
"server": self.ent_server.get(),
"port": int(self.ent_port.get()),
"email": self.ent_email.get(),
"password": self.ent_pass.get()
}
self.save_database()
self.ent_custom_sender.delete(0, tk.END)
self.ent_custom_sender.insert(0, self.ent_email.get())
messagebox.showinfo("Success", "SMTP configurations saved.")
def refresh_contact_list(self):
for item in self.tree.get_children():
self.tree.delete(item)
search_term = self.ent_search.get().strip().lower()
for c in self.db.get("contacts", []):
call = c.get("callsign", "").lower()
name = c.get("name", "").lower()
email = c.get("email", "").lower()
tags = c.get("tags", "").lower()
if search_term and (search_term not in call and search_term not in name and search_term not in email and search_term not in tags):
continue
e_ok = "YES" if c.get("receive_email", True) else "NO"
t_ok = "YES" if c.get("receive_text", True) else "NO"
self.tree.insert("", "end", values=(c.get("callsign", ""), c.get("name", ""), c.get("tags", ""), c.get("email", ""), c.get("phone_gateway", ""), e_ok, t_ok))
def import_google_csv(self):
import csv
file_path = filedialog.askopenfilename(filetypes=[("CSV Files", "*.csv")])
if not file_path:
return
try:
with open(file_path, mode='r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
headers = {k.lower(): k for k in reader.fieldnames}
name_key = headers.get('name') or headers.get('given name')
first_name_key = headers.get('given name') or headers.get('first name')
email_key = headers.get('e-mail 1 - value') or headers.get('email 1 - value') or headers.get('email')
phone_key = headers.get('phone 1 - value') or headers.get('phone')
notes_key = headers.get('notes')
group_key = headers.get('group membership')
imported_count = 0
for row in reader:
name = row.get(name_key, "").strip() if name_key else ""
first_name = row.get(first_name_key, "").strip() if first_name_key else ""
email = row.get(email_key, "").strip() if email_key else ""
phone = row.get(phone_key, "").strip() if phone_key else ""
notes = row.get(notes_key, "").strip() if notes_key else ""
g_membership = row.get(group_key, "").strip() if group_key else ""
if not name and not email:
continue
tags_detected = []
g_upper = g_membership.upper()
n_upper = notes.upper()
if "ARES" in g_upper or "ARES" in n_upper:
tags_detected.append("ARES")
if "RACES" in g_upper or "RACES" in n_upper:
tags_detected.append("RACES")
if "SKYWARN" in g_upper or "SKYWARN" in n_upper:
tags_detected.append("SKYWARN")
if "LEADER" in g_upper or "LEADER" in n_upper or "EC" in g_upper:
tags_detected.append("LEADER")
callsign = ""
if notes and ("/" in notes or len(notes) <= 6 and notes.isalnum()):
callsign = notes.upper()
elif "[" in name and "]" in name:
callsign = name[name.find("[")+1:name.find("]")].upper()
name = name.split("[")[0].strip()
if not callsign and first_name:
callsign = first_name
clean_phone = ''.join(c for c in phone if c.isdigit())
gateway = ""
if len(clean_phone) == 10:
gateway = f"{clean_phone}@vtext.com"
if not any(c['email'] == email for c in self.db['contacts'] if email):
self.db['contacts'].append({
"callsign": callsign if callsign else "VOLUNTEER",
"name": name,
"email": email,
"phone_gateway": gateway,
"tags": ", ".join(tags_detected),
"receive_email": True,
"receive_text": True if gateway else False
})
imported_count += 1
self.save_database()
self.refresh_contact_list()
self.update_dispatch_selectors()
messagebox.showinfo("Import Complete", f"Successfully imported {imported_count} contacts.")
except Exception as e:
messagebox.showerror("Import Error", f"Failed to parse Google Contacts CSV:\n{e}")
# --- TRANSMIT LAYERS ---
def process_dispatch_request(self):
cfg = self.db.get("smtp_settings", {})
if not cfg.get("email") or not cfg.get("password"):
messagebox.showwarning("Setup Required", "Please configure your SMTP credentials.")
return
raw_body = self.txt_message.get('1.0', tk.END).strip()
if not raw_body:
messagebox.showwarning("Empty Message", "Cannot send an empty alert dispatch text.")
return
# Plain Text Version without the unmonitored header
plain_body = raw_body
# HTML Version without the unmonitored header
html_body = raw_body.replace("\n", "<br>")
# Fetch dynamically typed sender address from form UI
display_sender = self.ent_custom_sender.get().strip()
if not display_sender:
messagebox.showwarning("Sender Address Missing", "Please provide a valid sender verification string.")
return
send_email_globally = self.dispatch_send_email.get()
send_text_globally = self.dispatch_send_text.get()
if not send_email_globally and not send_text_globally:
messagebox.showwarning("Selection Missing", "Please check at least one transmission mode to send.")
return
mode = self.target_mode.get()
target_list = []
if mode == "all":
target_list = self.db.get("contacts", [])
elif mode == "group":
selected_tag = self.cb_groups.get().strip().upper()
for c in self.db.get("contacts", []):
if c.get("tags"):
c_tags = [t.strip().upper() for t in c["tags"].split(",") if t.strip()]
if selected_tag in c_tags:
target_list.append(c)
elif mode == "individual":
selected_indiv = self.cb_individuals.get()
found = False
for c in self.db.get("contacts", []):
display_str = f"{c.get('callsign', 'VOLUNTEER')} - {c.get('name', '')} ({c.get('email', '')})"
if display_str == selected_indiv:
target_list = [c]
found = True
break
if not found:
messagebox.showwarning("Invalid Selection", "Please choose a valid individual entry.")
return
all_recipients = []
for contact in target_list:
if send_email_globally and contact.get("receive_email", True) and contact.get("email"):
all_recipients.append(contact["email"])
if send_text_globally and contact.get("receive_text", True) and contact.get("phone_gateway"):
all_recipients.append(contact["phone_gateway"])
all_recipients = list(set(all_recipients))
if not all_recipients:
messagebox.showwarning("No Recipients", "No targets available under selected permissions constraints.")
return
payload_tuple = (plain_body, html_body)
if self.sched_choice.get() == "now":
self.execute_transmission(all_recipients, payload_tuple, cfg, display_sender, None)
else:
date_str = self.ent_sched_date.get().strip()
time_str = self.ent_sched_time.get().strip()
try:
target_datetime = datetime.strptime(f"{date_str} {time_str}", "%m-%d-%y %H:%M")
delta_seconds = (target_datetime - datetime.now()).total_seconds()
if delta_seconds <= 0:
messagebox.showwarning("Invalid Time", "The scheduled time must be in the future.")
return
self.task_counter += 1
tid = f"TASK_{self.task_counter}"
t = threading.Timer(delta_seconds, self.execute_transmission, args=[all_recipients, payload_tuple, cfg, display_sender, tid])
self.active_schedulers[tid] = {
"timer": t,
"time": target_datetime.strftime("%m-%d-%y %H:%M:%S"),
"recipients_count": len(all_recipients)
}
t.start()
self.lbl_queue_status.config(text=f"Pending Alerts: {len(self.active_schedulers)}")
messagebox.showinfo("Alert Scheduled", f"Dispatch safely added to internal system queue.")
except ValueError:
messagebox.showerror("Format Error", "Date syntax must follow MM-DD-YY and time HH:MM format parameters.")
def execute_transmission(self, recipients, body_tuple, cfg, sender_override, task_id):
try:
plain_text, html_markup = body_tuple
if int(cfg["port"]) == 465:
server = smtplib.SMTP_SSL(cfg["server"], cfg["port"])
else:
server = smtplib.SMTP(cfg["server"], cfg["port"])
server.starttls()
server.login(cfg["email"], cfg["password"])
for rec in recipients:
msg = MIMEMultipart('alternative')
msg['From'] = sender_override
msg['To'] = rec
msg['Subject'] = "CCAR: MESSAGE ALERT PORTAL"
part1 = MIMEText(plain_text, 'plain')
part2 = MIMEText(html_markup, 'html')
msg.attach(part1)
msg.attach(part2)
server.send_message(msg)
server.quit()
if task_id and task_id in self.active_schedulers:
del self.active_schedulers[task_id]
self.root.after(0, self.update_queue_ui_label)
else:
messagebox.showinfo("Success", f"Immediate dispatch completed to {len(recipients)} endpoints.")
except Exception as e:
if threading.current_thread() == threading.main_thread():
messagebox.showerror("Transmission Failure", f"An error occurred while sending:\n{e}")
else:
print(f"[BACKGROUND DETECT WORKER ERROR]: {e}")
def update_queue_ui_label(self):
self.lbl_queue_status.config(text=f"Pending Alerts: {len(self.active_schedulers)}")
messagebox.showinfo("Queue System Update", "A scheduled group dispatch has deployed successfully.")
# --- PENDING QUEUE CANCELLATION SUB-SYSTEM ---
def open_queue_manager(self):
q_win = tk.Toplevel(self.root)
q_win.title("Active Task Queue Manager")
q_win.geometry("500x350")
q_win.grab_set()
ttk.Label(q_win, text="Active Pending Deployments:", font=('Helvetica', 10, 'bold')).pack(anchor='w', padx=10, pady=10)
lb_queue = tk.Listbox(q_win, selectmode="single", font=('Consolas', 10))
lb_queue.pack(fill='both', expand=True, padx=10, pady=5)
index_to_task_id = {}
for idx, (tid, details) in enumerate(self.active_schedulers.items()):
display_str = f"[{tid}] Time: {details['time']} | Recipients: {details['recipients_count']}"
lb_queue.insert(tk.END, display_str)
index_to_task_id[idx] = tid
def cancel_selected_task():
selection = lb_queue.curselection()
if not selection:
messagebox.showwarning("No Selection", "Please click a task inside the list viewport window first.")
return
chosen_idx = selection[0]
target_tid = index_to_task_id[chosen_idx]
task_dict = self.active_schedulers[target_tid]
task_dict["timer"].cancel()
del self.active_schedulers[target_tid]
self.lbl_queue_status.config(text=f"Pending Alerts: {len(self.active_schedulers)}")
q_win.destroy()
messagebox.showinfo("Task Aborted", f"Successfully terminated alert string identifier {target_tid} safely.")
btn_cancel = ttk.Button(q_win, text="Cancel Selected Alert", command=cancel_selected_task)
btn_cancel.pack(pady=10)
if __name__ == "__main__":
root = tk.Tk()
app = DispatchApp(root)
root.mainloop()