import os import json import uuid import time import pandas as pd import requests import base64 import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email import encoders from datetime import datetime from threading import Lock from flask import Flask, render_template, request, jsonify app = Flask(__name__) app.config['UPLOAD_FOLDER'] = 'uploads' os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) # In-memory storage for active campaigns (for simplicity) campaigns = {} campaigns_lock = Lock() @app.route('/') def dashboard(): return render_template('dashboard.html') @app.route('/campaign') def campaign(): return render_template('campaign.html') @app.route('/api/get_columns', methods=['POST']) def get_columns(): contacts_file = request.files.get('contactsFile') if not contacts_file: return jsonify({'error': 'No file uploaded'}), 400 file_ext = os.path.splitext(contacts_file.filename)[1].lower() if file_ext not in ['.csv', '.xlsx', '.xls']: return jsonify({'error': 'Invalid file type'}), 400 try: if file_ext == '.csv': df = pd.read_csv(contacts_file) else: df = pd.read_excel(contacts_file) columns = df.columns.tolist() return jsonify({'columns': columns}) except Exception as e: return jsonify({'error': str(e)}), 400 @app.route('/api/setup_campaign', methods=['POST']) def setup_campaign(): provider = request.form.get('provider') sender_name = request.form.get('sender_name') sender_email = request.form.get('sender_email') sender_password = request.form.get('sender_password') subject = request.form.get('subject') body = request.form.get('body') email_column = request.form.get('email_column', '').strip() contacts_file = request.files.get('contactsFile') if not contacts_file or contacts_file.filename == '': return jsonify({'error': 'Contacts file is required.'}), 400 if not email_column: return jsonify({'error': 'Please select an Email Column.'}), 400 # Secure filename and check extension allowed_extensions = {'.csv', '.xlsx', '.xls'} file_ext = os.path.splitext(contacts_file.filename)[1].lower() if file_ext not in allowed_extensions: return jsonify({'error': f'Invalid file type. Allowed: {", ".join(allowed_extensions)}'}), 400 # Basic input validation if not sender_email or "@" not in sender_email: return jsonify({'error': 'A valid sender email is required.'}), 400 if not sender_password or len(sender_password) < 8: return jsonify({'error': 'A valid App Password is required.'}), 400 if not subject or not body: return jsonify({'error': 'Subject and Body are required.'}), 400 # Verify API credentials BEFORE starting campaign if provider == 'resend' and not sender_password.startswith('re_'): return jsonify({'error': 'Invalid Resend API Key. It should start with "re_"'}), 400 if provider == 'sendgrid' and not sender_password.startswith('SG.'): return jsonify({'error': 'Invalid SendGrid API Key. It should start with "SG."'}), 400 if provider == 'gmail': try: server = smtplib.SMTP('smtp.gmail.com', 587, timeout=15) server.starttls() server.login(sender_email, sender_password) server.quit() except Exception as e: return jsonify({'error': f'SMTP Login Failed: Ensure 2-Step Verification is on and you are using an "App Password". Error: {str(e)}'}), 401 campaign_id = str(uuid.uuid4()) campaign_dir = os.path.join(app.config['UPLOAD_FOLDER'], campaign_id) os.makedirs(campaign_dir, exist_ok=True) contacts_path = os.path.join(campaign_dir, "contacts" + file_ext) contacts_file.save(contacts_path) # Handle attachments attachments = request.files.getlist('attachments') attachment_paths = [] for att in attachments: if att.filename: # Check attachment extension for safety att_ext = os.path.splitext(att.filename)[1].lower() if att_ext in {'.exe', '.bat', '.sh', '.msi'}: return jsonify({'error': f'Security Alert: File type {att_ext} is not allowed as an attachment.'}), 400 att_path = os.path.join(campaign_dir, att.filename) att.save(att_path) attachment_paths.append(att_path) # Parse Contacts try: if file_ext == '.csv': df = pd.read_csv(contacts_path) else: df = pd.read_excel(contacts_path) except Exception as e: return jsonify({'error': f'Failed to read contacts file: {str(e)}'}), 400 if email_column not in df.columns: return jsonify({'error': f'Excel/CSV must contain a "{email_column}" column.'}), 400 for col in ['EmailSent', 'DateSent', 'Notes']: if col not in df.columns: df[col] = '' df[col] = df[col].astype('object') # Get pending emails sent_emails = df[df['EmailSent'].astype(str).str.strip().str.lower() == 'yes'][email_column].tolist() pending_df = df[ (df['EmailSent'].astype(str).str.strip().str.lower() != 'yes') & (~df[email_column].isin(sent_emails)) ] pending_df = pending_df.drop_duplicates(subset=[email_column]) with campaigns_lock: campaigns[campaign_id] = { 'provider': provider, 'sender_name': sender_name, 'sender_email': sender_email, 'sender_password': sender_password, 'subject': subject, 'body': body, 'contacts_path': contacts_path, 'attachment_paths': attachment_paths, 'email_column': email_column, 'total_pending': len(pending_df), 'sent_count': 0, 'failed_count': 0, 'status': 'ready' } return jsonify({ 'campaign_id': campaign_id, 'total': len(df), 'pending': len(pending_df), 'already_sent': len(sent_emails) }) @app.route('/api/send_next', methods=['POST']) def send_next(): data = request.json campaign_id = data.get('campaign_id') with campaigns_lock: campaign = campaigns.get(campaign_id) if not campaign: return jsonify({'error': 'Campaign not found.'}), 404 # Load DF df = pd.read_excel(campaign['contacts_path']) if campaign['contacts_path'].endswith('.xlsx') else pd.read_csv(campaign['contacts_path']) email_column = campaign.get('email_column', 'RecipientEmail') sent_emails = df[df['EmailSent'].astype(str).str.strip().str.lower() == 'yes'][email_column].tolist() pending_df = df[ (df['EmailSent'].astype(str).str.strip().str.lower() != 'yes') & (~df[email_column].isin(sent_emails)) ] pending_df = pending_df.drop_duplicates(subset=[email_column]) if len(pending_df) == 0: return jsonify({'status': 'completed', 'sent': campaign['sent_count'], 'failed': campaign['failed_count']}) # Pick the first pending row = pending_df.iloc[0] idx = pending_df.index[0] recipient_email = row[email_column] # Personalize subject = campaign['subject'] body = campaign['body'] for col in df.columns: if pd.notna(row[col]): val = str(row[col]) subject = subject.replace(f'{{{col}}}', val) body = body.replace(f'{{{col}}}', val) # Send email success, message = _send_single_email( campaign.get('provider', 'resend'), campaign['sender_name'], campaign['sender_email'], campaign['sender_password'], recipient_email, subject, body, campaign['attachment_paths'] ) if success: df.at[idx, 'EmailSent'] = 'Yes' df.at[idx, 'DateSent'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') campaign['sent_count'] += 1 else: df.at[idx, 'Notes'] = f"Error: {message}" campaign['failed_count'] += 1 # Save back if campaign['contacts_path'].endswith('.xlsx'): df.to_excel(campaign['contacts_path'], index=False) else: df.to_csv(campaign['contacts_path'], index=False) time.sleep(1) # Minor delay for API rate limits return jsonify({ 'status': 'sending', 'progress': campaign['sent_count'] + campaign['failed_count'], 'total': campaign['total_pending'], 'last_recipient': recipient_email, 'success': success, 'message': message }) def _send_single_email(provider, s_name, s_email, api_key, to_email, subj, text, att_paths): try: # Prepare attachments in base64 attachments = [] for p in att_paths: if os.path.exists(p): with open(p, 'rb') as f: content = base64.b64encode(f.read()).decode('utf-8') filename = os.path.basename(p) attachments.append({"filename": filename, "content": content}) # Send using Resend API if provider == 'resend': headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = { "from": f"{s_name} <{s_email}>", "to": [to_email], "subject": subj, "html": text.replace('\n', '
') } if attachments: payload["attachments"] = attachments response = requests.post("https://api.resend.com/emails", json=payload, headers=headers, timeout=15) if response.status_code not in [200, 201, 202]: return False, f"Resend Error: {response.text}" # Send using SendGrid API elif provider == 'sendgrid': headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = { "personalizations": [{"to": [{"email": to_email}]}], "from": {"email": s_email, "name": s_name}, "subject": subj, "content": [{"type": "text/html", "value": text.replace('\n', '
')}] } if attachments: payload["attachments"] = [ { "content": att["content"], "filename": att["filename"], "disposition": "attachment" } for att in attachments ] response = requests.post("https://api.sendgrid.com/v3/mail/send", json=payload, headers=headers, timeout=15) if response.status_code not in [200, 201, 202]: return False, f"SendGrid Error: {response.text}" # Send using Gmail SMTP elif provider == 'gmail': msg = MIMEMultipart() msg['From'] = f"{s_name} <{s_email}>" msg['To'] = to_email msg['Subject'] = subj msg.attach(MIMEText(text, 'plain')) for p in att_paths: if os.path.exists(p): with open(p, 'rb') as f: part = MIMEBase('application', 'octet-stream') part.set_payload(f.read()) encoders.encode_base64(part) part.add_header('Content-Disposition', f'attachment; filename="{os.path.basename(p)}"') msg.attach(part) server = smtplib.SMTP('smtp.gmail.com', 587, timeout=15) server.starttls() server.login(s_email, api_key) server.send_message(msg) server.quit() return True, "Success" except Exception as e: return False, str(e) if __name__ == '__main__': app.run(host='0.0.0.0', port=7860)