File size: 12,249 Bytes
a5dfcf4
 
 
 
 
0084054
 
6fc5989
a5dfcf4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c777a12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a5dfcf4
 
0084054
a5dfcf4
 
 
 
 
4371a7d
c777a12
a5dfcf4
2d3eeb0
a5dfcf4
4371a7d
 
 
2d3eeb0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0084054
 
 
 
 
6fc5989
 
 
 
 
 
 
 
a5dfcf4
 
 
 
 
2d3eeb0
a5dfcf4
 
 
 
 
 
 
2d3eeb0
 
 
 
 
a5dfcf4
 
 
 
 
 
2d3eeb0
a5dfcf4
 
 
 
 
 
c777a12
 
a5dfcf4
 
 
 
 
 
 
c777a12
a5dfcf4
 
c777a12
a5dfcf4
c777a12
a5dfcf4
 
 
0084054
a5dfcf4
 
 
 
 
 
 
c777a12
a5dfcf4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c777a12
a5dfcf4
c777a12
a5dfcf4
 
c777a12
a5dfcf4
c777a12
a5dfcf4
 
 
 
 
 
 
c777a12
a5dfcf4
 
 
 
 
 
 
 
 
 
 
 
0084054
a5dfcf4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0084054
a5dfcf4
0084054
 
a5dfcf4
 
0084054
 
 
 
a5dfcf4
0084054
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6fc5989
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a5dfcf4
 
 
 
 
c777a12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
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', '<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)