Skip to content

Commit a5b783e

Browse files
authored
Create SpamSentinel.py
1 parent 6487f1e commit a5b783e

1 file changed

Lines changed: 371 additions & 0 deletions

File tree

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
"""
2+
SpamSentinel v1.0 - Enterprise Edition
3+
Ultra-Fast Email Spam Detector with GUI
4+
Supports massive email sets with smooth UI
5+
"""
6+
7+
import os, sys, re, threading
8+
from collections import Counter, deque
9+
import tkinter as tk
10+
from tkinter import filedialog, messagebox, ttk
11+
12+
import ttkbootstrap as tb
13+
from ttkbootstrap.constants import *
14+
15+
try:
16+
from tkinterdnd2 import TkinterDnD, DND_FILES
17+
DND_ENABLED = True
18+
except ImportError:
19+
DND_ENABLED = False
20+
print("Drag & Drop requires tkinterdnd2: pip install tkinterdnd2")
21+
22+
23+
# ---------------------- UTIL ----------------------
24+
def resource_path(file_name):
25+
base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
26+
return os.path.join(base_path, file_name)
27+
28+
29+
# ---------------------- SPAM DETECTION WORKER ----------------------
30+
class SpamWorker:
31+
def __init__(self, files, min_confidence, include_words,
32+
exclude_words, regex_pattern, max_results,
33+
callbacks):
34+
self.files = files
35+
self.min_confidence = min_confidence
36+
self.include_words = include_words
37+
self.exclude_words = exclude_words
38+
self.regex_pattern = re.compile(regex_pattern, re.IGNORECASE) if regex_pattern else None
39+
self.max_results = max_results
40+
self._running = True
41+
self.callbacks = callbacks
42+
43+
# Fake spam scoring keywords
44+
self.spam_patterns = [
45+
re.compile(r"(free money|win cash|click here|urgent|lottery)", re.I),
46+
re.compile(r"(prize|offer|risk-free|credit card)", re.I),
47+
]
48+
49+
def stop(self):
50+
self._running = False
51+
52+
def spam_score(self, text):
53+
score = 0
54+
for pattern in self.spam_patterns:
55+
if pattern.search(text):
56+
score += 50
57+
return min(score, 100) # max 100
58+
59+
def run(self):
60+
total_files = len(self.files)
61+
counters = Counter()
62+
results_buffer = deque(maxlen=self.max_results)
63+
64+
for i, path in enumerate(self.files):
65+
if not self._running:
66+
break
67+
try:
68+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
69+
content = f.read()
70+
if self.include_words and not any(w in content.lower() for w in self.include_words):
71+
continue
72+
if self.exclude_words and any(w in content.lower() for w in self.exclude_words):
73+
continue
74+
if self.regex_pattern and not self.regex_pattern.search(content):
75+
continue
76+
77+
score = self.spam_score(content)
78+
if score < self.min_confidence:
79+
continue
80+
81+
counters["SPAM"] += 1
82+
counters["TOTAL"] += 1
83+
results_buffer.append((path, score))
84+
85+
if "found" in self.callbacks:
86+
self.callbacks["found"](path, score)
87+
88+
if counters["TOTAL"] >= self.max_results:
89+
break
90+
except Exception:
91+
pass
92+
93+
if total_files > 0 and "progress" in self.callbacks:
94+
self.callbacks["progress"](int((i + 1) / total_files * 100))
95+
if "stats" in self.callbacks:
96+
self.callbacks["stats"](dict(counters))
97+
98+
if "stats" in self.callbacks:
99+
self.callbacks["stats"](dict(counters))
100+
if "finished" in self.callbacks:
101+
self.callbacks["finished"]()
102+
103+
104+
# ---------------------- MAIN APP ----------------------
105+
class SpamSentinelApp:
106+
APP_NAME = "SpamSentinel"
107+
APP_VERSION = "1.0"
108+
SUPPORTED_EXT = (".eml", ".txt")
109+
110+
def __init__(self):
111+
if DND_ENABLED:
112+
self.root = TkinterDnD.Tk()
113+
else:
114+
self.root = tb.Window(themename="darkly")
115+
116+
self.root.title(f"{self.APP_NAME} v{self.APP_VERSION}")
117+
self.root.minsize(1200, 700)
118+
119+
try:
120+
self.root.iconbitmap(resource_path("logo.ico"))
121+
except: pass
122+
123+
self.worker_obj = None
124+
self.smooth_value = 0
125+
self.target_progress = 0
126+
self.file_set = set()
127+
128+
# Checkbox variables
129+
self.eml_var = tk.BooleanVar(value=True)
130+
self.txt_var = tk.BooleanVar(value=True)
131+
132+
self._build_ui()
133+
self._apply_styles()
134+
135+
# ---------------------- UI ----------------------
136+
def _build_ui(self):
137+
main = tb.Frame(self.root, padding=10)
138+
main.pack(fill=BOTH, expand=True)
139+
140+
tb.Label(main, text=f"🧠 {self.APP_NAME} - Enterprise Spam Detector",
141+
font=("Segoe UI", 22, "bold")).pack(pady=(0, 4))
142+
143+
tb.Label(main, text="Detect spam emails quickly and efficiently",
144+
font=("Segoe UI", 10, "italic"), foreground="#9ca3af").pack(pady=(0, 20))
145+
146+
# Row 1: File Selection
147+
row1 = tb.Frame(main)
148+
row1.pack(fill=X, pady=(0,6))
149+
150+
self.path_input = tb.Entry(row1, width=90)
151+
self.path_input.pack(side=LEFT, fill=X, expand=True, padx=(0,6))
152+
self.path_input.insert(0, "Drag & drop email files or folders here…")
153+
154+
browse_btn = tb.Button(row1, text="📂 Browse", bootstyle=INFO, command=self.browse)
155+
browse_btn.pack(side=LEFT, padx=3)
156+
157+
self.start_btn = tb.Button(row1, text="🚀 Start Scan", bootstyle=SUCCESS, command=self.start)
158+
self.start_btn.pack(side=LEFT, padx=3)
159+
160+
self.cancel_btn = tb.Button(row1, text="⏹ Cancel", bootstyle=DANGER, command=self.cancel)
161+
self.cancel_btn.pack(side=LEFT, padx=3)
162+
self.cancel_btn.config(state=DISABLED)
163+
164+
export_btn = tb.Button(row1, text="💾 Export Selected", bootstyle=PRIMARY, command=self.export_results)
165+
export_btn.pack(side=LEFT, padx=3)
166+
167+
about_btn = tb.Button(row1, text="ℹ️ About", bootstyle=INFO, command=self.show_about)
168+
about_btn.pack(side=LEFT, padx=3)
169+
170+
# Row 2: Filters
171+
row2 = tb.Frame(main)
172+
row2.pack(fill=X, pady=(0,6))
173+
174+
self.eml_cb = tb.Checkbutton(row2, text=".eml", bootstyle=SUCCESS, variable=self.eml_var)
175+
self.eml_cb.pack(side=LEFT, padx=3)
176+
self.txt_cb = tb.Checkbutton(row2, text=".txt", bootstyle=SUCCESS, variable=self.txt_var)
177+
self.txt_cb.pack(side=LEFT, padx=3)
178+
179+
tb.Label(row2, text="Min Spam Confidence (%)").pack(side=LEFT, padx=3)
180+
self.confidence_input = tb.Entry(row2, width=5)
181+
self.confidence_input.insert(0, "50")
182+
self.confidence_input.pack(side=LEFT, padx=3)
183+
184+
tb.Label(row2, text="Include words").pack(side=LEFT, padx=3)
185+
self.include_input = tb.Entry(row2, width=20)
186+
self.include_input.pack(side=LEFT, padx=3)
187+
188+
tb.Label(row2, text="Exclude words").pack(side=LEFT, padx=3)
189+
self.exclude_input = tb.Entry(row2, width=20)
190+
self.exclude_input.pack(side=LEFT, padx=3)
191+
192+
tb.Label(row2, text="Regex filter (optional)").pack(side=LEFT, padx=3)
193+
self.regex_input = tb.Entry(row2, width=20)
194+
self.regex_input.pack(side=LEFT, padx=3)
195+
196+
# Progress
197+
self.progress = tb.Progressbar(main, bootstyle="success-striped", maximum=100)
198+
self.progress.pack(fill=X, pady=(0,6))
199+
200+
# Treeview for multi-select batch
201+
columns = ("selected", "filename", "spam_score")
202+
self.tree = ttk.Treeview(main, columns=columns, show="headings", selectmode="extended", height=20)
203+
self.tree.heading("selected", text="✅")
204+
self.tree.heading("filename", text="Filename", anchor=W)
205+
self.tree.heading("spam_score", text="Spam Score (%)", anchor=W)
206+
self.tree.column("selected", width=50, anchor=CENTER)
207+
self.tree.column("filename", width=800)
208+
self.tree.column("spam_score", width=120)
209+
self.tree.pack(fill=BOTH, expand=True, pady=(0,6))
210+
211+
self.stats_label = tb.Label(main, text="TOTAL: 0 | SPAM: 0")
212+
self.stats_label.pack(anchor=E)
213+
214+
# Timer for smooth progress
215+
self.root.after(15, self.animate_progress)
216+
217+
# Bind DnD if enabled
218+
if DND_ENABLED:
219+
self.tree.drop_target_register(DND_FILES)
220+
self.tree.dnd_bind("<<Drop>>", self.on_drop)
221+
222+
# ---------------------- Unified Browse + DnD ----------------------
223+
def browse(self):
224+
folder = filedialog.askdirectory(title="Select Email Folder")
225+
if folder:
226+
self.start_btn.config(state=DISABLED)
227+
self.cancel_btn.config(state=NORMAL)
228+
threading.Thread(target=self._scan_and_queue_files_thread, args=([folder],), daemon=True).start()
229+
230+
def on_drop(self, event):
231+
dropped_paths = self.root.tk.splitlist(event.data)
232+
self.start_btn.config(state=DISABLED)
233+
self.cancel_btn.config(state=NORMAL)
234+
threading.Thread(target=self._scan_and_queue_files_thread, args=(dropped_paths,), daemon=True).start()
235+
236+
def _scan_and_queue_files_thread(self, paths):
237+
all_files = []
238+
for p in paths:
239+
if os.path.isdir(p):
240+
for root_dir, _, fs in os.walk(p):
241+
for name in fs:
242+
all_files.append(os.path.join(root_dir, name))
243+
elif os.path.isfile(p):
244+
all_files.append(p)
245+
self._insert_files_chunked(all_files)
246+
247+
def _insert_files_chunked(self, paths, chunk_size=500):
248+
if not paths:
249+
self.start_btn.config(state=NORMAL)
250+
self.cancel_btn.config(state=DISABLED)
251+
self.path_input.delete(0, END)
252+
self.path_input.insert(0, f"{len(self.file_set)} files queued")
253+
return
254+
chunk = paths[:chunk_size]
255+
remaining = paths[chunk_size:]
256+
for path in chunk:
257+
ext = os.path.splitext(path)[1].lower()
258+
if ((ext == ".eml" and self.eml_var.get()) or (ext == ".txt" and self.txt_var.get())):
259+
if path not in self.file_set:
260+
self.file_set.add(path)
261+
self.tree.insert("", END, values=("☑️", path, "0"))
262+
self.root.after(1, lambda: self._insert_files_chunked(remaining, chunk_size))
263+
264+
# ---------------------- Actions ----------------------
265+
def start(self):
266+
selected_files = [self.tree.item(i)['values'][1] for i in self.tree.get_children()
267+
if self.tree.item(i)['values'][0]=="☑️"]
268+
if not selected_files:
269+
messagebox.showwarning("No Selection", "Select emails using the checkboxes before scanning.")
270+
return
271+
try:
272+
min_conf = int(self.confidence_input.get())
273+
except ValueError:
274+
min_conf = 50
275+
self.progress["value"] = 0
276+
self.smooth_value = 0
277+
self.target_progress = 0
278+
self.start_btn.config(state=DISABLED)
279+
self.cancel_btn.config(state=NORMAL)
280+
threading.Thread(target=self._run_worker, args=(selected_files, min_conf), daemon=True).start()
281+
282+
def _run_worker(self, files, min_confidence):
283+
self.worker_obj = SpamWorker(
284+
files,
285+
min_confidence,
286+
[w.strip() for w in self.include_input.get().lower().split(",") if w.strip()],
287+
[w.strip() for w in self.exclude_input.get().lower().split(",") if w.strip()],
288+
self.regex_input.get(),
289+
max_results=200_000,
290+
callbacks={
291+
"found": self.add_line,
292+
"progress": self.set_target,
293+
"stats": self.update_stats,
294+
"finished": self.finish
295+
}
296+
)
297+
self.worker_obj.run()
298+
299+
def add_line(self, file, score):
300+
for i in self.tree.get_children():
301+
if self.tree.item(i)['values'][1] == file:
302+
self.tree.item(i, values=("☑️", file, str(score)))
303+
color = "#dc2626" if score >= 70 else "#facc15" if score >= 50 else "#4ade80"
304+
self.tree.tag_configure(file, foreground=color)
305+
self.tree.item(i, tags=(file,))
306+
break
307+
308+
def update_stats(self, stats):
309+
self.stats_label.config(text=f"TOTAL: {stats.get('TOTAL',0)} | SPAM: {stats.get('SPAM',0)}")
310+
311+
def set_target(self, v):
312+
self.target_progress = v
313+
314+
def animate_progress(self):
315+
if self.smooth_value < self.target_progress:
316+
self.smooth_value += 1
317+
self.progress["value"] = self.smooth_value
318+
self.root.after(15, self.animate_progress)
319+
320+
def cancel(self):
321+
if self.worker_obj:
322+
self.worker_obj.stop()
323+
self.finish()
324+
325+
def finish(self):
326+
self.start_btn.config(state=NORMAL)
327+
self.cancel_btn.config(state=DISABLED)
328+
self.progress["value"] = 100
329+
330+
# ---------------------- Export ----------------------
331+
def export_results(self):
332+
selected_files = [self.tree.item(i)['values'] for i in self.tree.get_children()
333+
if self.tree.item(i)['values'][0]=="☑️"]
334+
if not selected_files:
335+
messagebox.showwarning("Export", "No selected files to export")
336+
return
337+
path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text Files","*.txt")])
338+
if path:
339+
with open(path,"w",encoding="utf-8") as f:
340+
for s in selected_files:
341+
f.write(f"{s[1]} | Spam Score: {s[2]}%\n")
342+
messagebox.showinfo("Export", "Export completed")
343+
344+
# ---------------------- About ----------------------
345+
def show_about(self):
346+
messagebox.showinfo(
347+
f"About {self.APP_NAME}",
348+
f"{self.APP_NAME} v{self.APP_VERSION}\n\n"
349+
"• Drag & drop email files or folders\n"
350+
"• Multi-select batch scanning with checkboxes\n"
351+
"• Real-time spam score highlighting\n"
352+
"• Include/exclude filters & optional regex\n"
353+
"• Export selected results to text\n\n"
354+
"🏢 Built by Mate Technologies\n"
355+
"🌐 https://matetools.gumroad.com"
356+
)
357+
358+
# ---------------------- Styles ----------------------
359+
def _apply_styles(self):
360+
self.root.style = tb.Style(theme="darkly")
361+
self.root.style.configure("TProgressbar", troughcolor="#1b1f3a", background="#7c3aed", thickness=14)
362+
363+
# ---------------------- Run ----------------------
364+
def run(self):
365+
self.root.mainloop()
366+
367+
368+
# ---------------------- RUN ----------------------
369+
if __name__ == "__main__":
370+
app = SpamSentinelApp()
371+
app.run()

0 commit comments

Comments
 (0)