Spaces:
Sleeping
Sleeping
| 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() | |
| def dashboard(): | |
| return render_template('dashboard.html') | |
| def campaign(): | |
| return render_template('campaign.html') | |
| 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 | |
| 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) | |
| }) | |
| 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', '<br>') | |
| } | |
| 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', '<br>')}] | |
| } | |
| 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) | |