194 lines
5.7 KiB
Python
194 lines
5.7 KiB
Python
# verify-app.py (with filtered CSV downloads)
|
|
|
|
import csv
|
|
import io
|
|
import re
|
|
import time
|
|
import uuid
|
|
import dns.resolver
|
|
import smtplib
|
|
from flask import Flask, request, jsonify, send_file, Response
|
|
from flask_cors import CORS
|
|
from tempfile import NamedTemporaryFile
|
|
|
|
app = Flask(__name__)
|
|
CORS(app)
|
|
|
|
print("\U0001F525 VERIFIER RUNNING - Want sales calls from leads? Go to AlexBerman.com/Mastermind \U0001F525")
|
|
|
|
EMAIL_REGEX = re.compile(r"[^@]+@[^@]+\.[^@]+")
|
|
DISPOSABLE_DOMAINS = {"mailinator.com", "10minutemail.com", "guerrillamail.com"}
|
|
ROLE_BASED_PREFIXES = {"info", "support", "admin", "sales", "contact"}
|
|
|
|
data = {}
|
|
|
|
def check_email(email):
|
|
import time
|
|
|
|
if not EMAIL_REGEX.match(email):
|
|
return "invalid", "bad_syntax"
|
|
|
|
domain = email.split('@')[1]
|
|
local = email.split('@')[0]
|
|
|
|
if domain.lower() in DISPOSABLE_DOMAINS:
|
|
return "invalid", "disposable_domain"
|
|
if local.lower() in ROLE_BASED_PREFIXES:
|
|
return "invalid", "role_based"
|
|
|
|
try:
|
|
records = dns.resolver.resolve(domain, 'MX')
|
|
mx_record = str(records[0].exchange)
|
|
except Exception:
|
|
return "invalid", "no_mx"
|
|
|
|
try:
|
|
server = smtplib.SMTP(timeout=10)
|
|
server.connect(mx_record)
|
|
server.helo("example.com")
|
|
server.mail("probe@example.com")
|
|
code, _ = server.rcpt(f"doesnotexist123@{domain}")
|
|
server.quit()
|
|
if code == 250:
|
|
return "risky", "domain_accepts_all"
|
|
except Exception:
|
|
pass
|
|
|
|
def smtp_check():
|
|
try:
|
|
server = smtplib.SMTP(timeout=10)
|
|
server.connect(mx_record)
|
|
server.helo("example.com")
|
|
server.mail("verifier@example.com")
|
|
code, _ = server.rcpt(email)
|
|
server.quit()
|
|
return code
|
|
except Exception:
|
|
return None
|
|
|
|
code = smtp_check()
|
|
if code in [421, 450, 451, 452, 503]:
|
|
time.sleep(5)
|
|
code = smtp_check()
|
|
|
|
if code == 250:
|
|
return "valid", "smtp_ok"
|
|
elif code is None:
|
|
return "risky", "smtp_timeout"
|
|
elif code in [421, 450, 451, 452, 503]:
|
|
return "risky", f"smtp_soft_fail_{code}"
|
|
elif code == 550:
|
|
return "invalid", "smtp_reject"
|
|
else:
|
|
return "invalid", f"smtp_{code}"
|
|
|
|
@app.route('/verify', methods=['POST'])
|
|
def verify():
|
|
job_id = str(uuid.uuid4())
|
|
file = request.files['file']
|
|
content = file.read().decode('utf-8')
|
|
reader = list(csv.DictReader(io.StringIO(content)))
|
|
total = len(reader)
|
|
email_field = next((f for f in reader[0].keys() if f.lower().strip() == 'email'), None)
|
|
|
|
output = io.StringIO()
|
|
fieldnames = list(reader[0].keys()) + ['status', 'reason']
|
|
writer = csv.DictWriter(output, fieldnames=fieldnames)
|
|
writer.writeheader()
|
|
|
|
data[job_id] = {
|
|
"progress": 0,
|
|
"row": 0,
|
|
"total": total,
|
|
"log": "",
|
|
"cancel": False,
|
|
"output": output,
|
|
"writer": writer,
|
|
"records": reader,
|
|
"email_field": email_field,
|
|
"filename": file.filename
|
|
}
|
|
|
|
def run():
|
|
for i, row in enumerate(reader, start=1):
|
|
if data[job_id]['cancel']:
|
|
data[job_id]['log'] = f"\u274c Canceled job {job_id}"
|
|
break
|
|
email = (row.get(email_field) or '').strip()
|
|
if not email:
|
|
status, reason = 'invalid', 'empty_email'
|
|
else:
|
|
status, reason = check_email(email)
|
|
row['status'], row['reason'] = status, reason
|
|
writer.writerow(row)
|
|
percent = int((i / total) * 100)
|
|
data[job_id].update({"progress": percent, "row": i,
|
|
"log": f"\u2705 {email} → {status} ({reason})"})
|
|
output = data[job_id]['output']
|
|
output.seek(0)
|
|
temp = NamedTemporaryFile(delete=False, suffix=".csv", mode='w+')
|
|
temp.write(output.read())
|
|
temp.flush()
|
|
temp.seek(0)
|
|
data[job_id]['file_path'] = temp.name
|
|
|
|
import threading
|
|
threading.Thread(target=run).start()
|
|
|
|
return jsonify({"job_id": job_id})
|
|
|
|
@app.route('/progress')
|
|
def progress():
|
|
job_id = request.args.get("job_id")
|
|
d = data.get(job_id, {})
|
|
return jsonify({"percent": d.get("progress", 0), "row": d.get("row", 0), "total": d.get("total", 0)})
|
|
|
|
@app.route('/log')
|
|
def log():
|
|
job_id = request.args.get("job_id")
|
|
return Response(data.get(job_id, {}).get("log", ""), mimetype='text/plain')
|
|
|
|
@app.route('/cancel', methods=['POST'])
|
|
def cancel():
|
|
job_id = request.args.get("job_id")
|
|
if job_id in data:
|
|
data[job_id]['cancel'] = True
|
|
return '', 204
|
|
|
|
@app.route('/download')
|
|
def download():
|
|
job_id = request.args.get("job_id")
|
|
filter_type = request.args.get("type", "all")
|
|
job = data.get(job_id)
|
|
if not job:
|
|
return "Invalid job ID", 404
|
|
|
|
job['output'].seek(0)
|
|
reader = list(csv.DictReader(job['output']))
|
|
|
|
if filter_type == "valid":
|
|
filtered = [row for row in reader if row['status'] == 'valid']
|
|
elif filter_type == "risky":
|
|
filtered = [row for row in reader if row['status'] == 'risky']
|
|
elif filter_type == "risky_invalid":
|
|
filtered = [row for row in reader if row['status'] in ('risky', 'invalid')]
|
|
else:
|
|
filtered = reader
|
|
|
|
output = io.StringIO()
|
|
writer = csv.DictWriter(output, fieldnames=reader[0].keys())
|
|
writer.writeheader()
|
|
for row in filtered:
|
|
writer.writerow(row)
|
|
|
|
output.seek(0)
|
|
download_name = f"{filter_type}-galadon-{job['filename']}"
|
|
return Response(
|
|
output.getvalue(),
|
|
mimetype='text/csv',
|
|
headers={"Content-Disposition": f"attachment; filename={download_name}"}
|
|
)
|
|
|
|
if __name__ == '__main__':
|
|
app.run(debug=True, port=5050)
|