File size: 32,653 Bytes
17d6e24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c69a5b
 
 
 
17d6e24
 
 
 
 
 
7d13f48
 
 
 
 
 
 
 
 
 
 
 
 
17d6e24
 
 
 
 
 
 
8c69a5b
 
 
 
7d13f48
17d6e24
 
 
 
 
 
7d13f48
 
 
17d6e24
 
 
 
 
 
 
 
 
 
 
 
 
76b796f
17d6e24
 
 
6b9625f
76b796f
6b9625f
 
 
 
 
 
76b796f
6b9625f
 
76b796f
6b9625f
 
76b796f
6b9625f
17d6e24
 
 
76b796f
17d6e24
 
 
 
 
 
 
 
 
 
 
 
76b796f
 
 
 
17d6e24
 
76b796f
 
17d6e24
 
 
 
 
 
 
 
 
76b796f
17d6e24
 
76b796f
17d6e24
 
 
 
 
 
 
76b796f
 
 
17d6e24
76b796f
17d6e24
 
 
76b796f
17d6e24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6369537
17d6e24
6369537
 
17d6e24
 
 
 
7d13f48
17d6e24
7d13f48
 
17d6e24
 
 
 
 
6369537
 
 
 
 
17d6e24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6369537
 
17d6e24
 
 
 
 
 
6369537
17d6e24
 
6369537
 
 
17d6e24
8c69a5b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17d6e24
8c69a5b
17d6e24
6369537
 
 
 
 
 
17d6e24
 
76b796f
6369537
 
 
 
 
 
 
 
17d6e24
 
 
 
 
 
 
6369537
 
 
 
 
17d6e24
 
 
6369537
 
17d6e24
 
 
6369537
 
 
17d6e24
 
6369537
 
 
17d6e24
 
 
 
 
8c69a5b
 
6369537
 
76b796f
 
8c69a5b
 
76b796f
8c69a5b
 
7d13f48
6369537
8c69a5b
76b796f
17d6e24
6369537
 
76b796f
 
 
 
 
 
8c69a5b
76b796f
7d13f48
76b796f
6369537
 
76b796f
8c69a5b
 
 
 
6369537
8c69a5b
76b796f
 
8c69a5b
6369537
8c69a5b
 
76b796f
 
 
8c69a5b
6369537
 
76b796f
 
8c69a5b
 
76b796f
8c69a5b
6369537
 
76b796f
 
6369537
8c69a5b
17d6e24
 
8c69a5b
76b796f
 
 
8c69a5b
 
17d6e24
6369537
8c69a5b
76b796f
 
 
7d13f48
8c69a5b
76b796f
 
 
 
 
6369537
76b796f
 
 
 
 
8c69a5b
 
 
6369537
 
17d6e24
 
 
76b796f
 
 
17d6e24
6369537
17d6e24
 
 
 
 
 
 
 
76b796f
17d6e24
 
 
 
6369537
17d6e24
6369537
17d6e24
 
 
6369537
 
 
 
 
17d6e24
 
 
 
 
 
6369537
 
 
 
 
17d6e24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d13f48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17d6e24
 
 
 
 
7d13f48
17d6e24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6b9625f
17d6e24
 
 
8c69a5b
 
 
 
 
 
 
 
17d6e24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
import gradio as gr
import pandas as pd
import io
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
import os
from datetime import datetime
from docx import Document
import tempfile
import subprocess
import platform
import zipfile
import shutil
import threading
import queue
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

# Global variables for email configuration and positioning
email_config = {
    'smtp_server': '',
    'smtp_port': 587,
    'sender_email': '',
    'sender_password': '',
    'email_subject': 'Your Certificate of Participation',
    'email_body': '''Dear {name},



Congratulations on successfully completing the webinar on "Modern Project Management Trends and Global Opportunities for Project Management"!



Please find your certificate of participation attached to this email.



This 2 PDU Programme is accepted by PMI-USA (PDU Code: 25675RL34G).



Best regards,

Pro Consultancy International (Pvt.) Ltd

PMI Authorized Training Partner'''
}

# Text replacement configuration
text_replacement_config = {
    'placeholder_text': 'Name'  # Text to replace in Word document
}

# Email rate limiting configuration
EMAIL_DELAY = 2  # Seconds between emails to avoid being flagged as spam
MAX_WORKERS = 3  # Maximum parallel threads for certificate generation

def setup_email_server(smtp_server, smtp_port, sender_email, sender_password, email_subject, email_body):
    """Configure email server settings"""
    global email_config
    email_config = {
        'smtp_server': smtp_server,
        'smtp_port': int(smtp_port),
        'sender_email': sender_email,
        'sender_password': sender_password,
        'email_subject': email_subject,
        'email_body': email_body
    }
    return "βœ… Email server configured successfully!"

def setup_text_replacement(placeholder_text):
    """Configure text replacement for Word documents"""
    global text_replacement_config
    text_replacement_config['placeholder_text'] = placeholder_text
    return f"βœ… Placeholder text set to: '{placeholder_text}'"

def convert_docx_to_pdf(docx_path):
    """Convert Word document to PDF using LibreOffice or system tools"""
    try:
        output_pdf = docx_path.replace('.docx', '.pdf')
        print(f"πŸ“„ Converting {os.path.basename(docx_path)} to PDF...")
        
        # Try LibreOffice conversion (works on Linux/HuggingFace)
        if platform.system() != 'Windows':
            try:
                print("🐧 Using LibreOffice (Linux)...")
                result = subprocess.run([
                    'libreoffice', '--headless', '--convert-to', 'pdf',
                    '--outdir', os.path.dirname(docx_path), docx_path
                ], check=True, capture_output=True, timeout=30)
                
                if os.path.exists(output_pdf):
                    print(f"βœ… PDF created: {os.path.basename(output_pdf)}")
                    return output_pdf
                else:
                    print(f"⚠️ LibreOffice: PDF not created, using DOCX")
                    return docx_path
            except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired) as e:
                print(f"⚠️ LibreOffice not available or failed: {e}. Using DOCX file.")
                return docx_path
        else:
            # For Windows, try using win32com with proper error handling
            try:
                print("πŸͺŸ Using win32com (Windows)...")
                import win32com.client
                import pythoncom
                import time
                
                pythoncom.CoInitialize()
                word = None
                try:
                    word = win32com.client.DispatchEx('Word.Application')
                    word.Visible = False
                    word.DisplayAlerts = 0
                    
                    # Open document
                    abs_docx = os.path.abspath(docx_path)
                    abs_pdf = os.path.abspath(output_pdf)
                    print(f"Opening: {abs_docx}")
                    doc = word.Documents.Open(abs_docx)
                    
                    # Save as PDF (wdFormatPDF = 17)
                    print(f"Saving as: {abs_pdf}")
                    doc.SaveAs2(abs_pdf, FileFormat=17)
                    
                    # Close document
                    doc.Close(SaveChanges=False)
                    
                    # Wait a bit for file to be written
                    time.sleep(0.5)
                    
                    # Check if PDF was created
                    if os.path.exists(output_pdf):
                        print(f"βœ… PDF created: {os.path.basename(output_pdf)}")
                        return output_pdf
                    else:
                        print(f"❌ PDF not created: {output_pdf}")
                        return docx_path
                        
                finally:
                    if word:
                        word.Quit()
                    pythoncom.CoUninitialize()
                    
            except ImportError as ie:
                print(f"⚠️ win32com not installed: {ie}. Returning DOCX file.")
                return docx_path
            except Exception as e:
                print(f"❌ Win32com PDF conversion error: {e}")
                # If conversion fails, return docx path
                return docx_path
    except Exception as e:
        print(f"❌ PDF conversion error: {e}")
        return docx_path

def replace_text_in_docx(docx_path, placeholder, replacement):
    """Replace placeholder text in Word document with student name"""
    doc = Document(docx_path)
    
    # Replace in paragraphs - handle runs properly
    for paragraph in doc.paragraphs:
        if placeholder in paragraph.text:
            # Get the full text
            full_text = paragraph.text
            if placeholder in full_text:
                # Replace in the full text
                new_text = full_text.replace(placeholder, replacement)
                
                # Clear all runs and add new text with first run's formatting
                if paragraph.runs:
                    first_run = paragraph.runs[0]
                    # Store formatting
                    font_name = first_run.font.name
                    font_size = first_run.font.size
                    font_bold = first_run.font.bold
                    font_italic = first_run.font.italic
                    font_color = first_run.font.color.rgb if first_run.font.color.rgb else None
                    
                    # Clear all runs
                    for run in paragraph.runs:
                        run.text = ""
                    
                    # Set new text in first run
                    paragraph.runs[0].text = new_text
                    paragraph.runs[0].font.name = font_name
                    paragraph.runs[0].font.size = font_size
                    paragraph.runs[0].font.bold = font_bold
                    paragraph.runs[0].font.italic = font_italic
                    if font_color:
                        paragraph.runs[0].font.color.rgb = font_color
    
    # Replace in tables
    for table in doc.tables:
        for row in table.rows:
            for cell in row.cells:
                for paragraph in cell.paragraphs:
                    if placeholder in paragraph.text:
                        # Get the full text
                        full_text = paragraph.text
                        if placeholder in full_text:
                            # Replace in the full text
                            new_text = full_text.replace(placeholder, replacement)
                            
                            # Clear all runs and add new text with first run's formatting
                            if paragraph.runs:
                                first_run = paragraph.runs[0]
                                # Store formatting
                                font_name = first_run.font.name
                                font_size = first_run.font.size
                                font_bold = first_run.font.bold
                                font_italic = first_run.font.italic
                                font_color = first_run.font.color.rgb if first_run.font.color.rgb else None
                                
                                # Clear all runs
                                for run in paragraph.runs:
                                    run.text = ""
                                
                                # Set new text in first run
                                paragraph.runs[0].text = new_text
                                paragraph.runs[0].font.name = font_name
                                paragraph.runs[0].font.size = font_size
                                paragraph.runs[0].font.bold = font_bold
                                paragraph.runs[0].font.italic = font_italic
                                if font_color:
                                    paragraph.runs[0].font.color.rgb = font_color
    
    return doc

def load_template(template_file):
    """Load Word document template"""
    if template_file is None:
        return None
    
    file_ext = os.path.splitext(template_file)[1].lower()
    
    if file_ext in ['.docx', '.doc']:
        return template_file
    else:
        return None

def generate_certificate(template_path, name):
    """Generate certificate by replacing text in Word document"""
    temp_dir = tempfile.mkdtemp()
    output_docx = os.path.join(temp_dir, f"Certificate_{name.replace(' ', '_')}.docx")
    
    # Replace text in Word document
    doc = replace_text_in_docx(template_path, text_replacement_config['placeholder_text'], name)
    doc.save(output_docx)
    
    # Convert to PDF
    output_pdf = convert_docx_to_pdf(output_docx)
    
    return output_pdf

def preview_with_test_name(template_file, test_name, placeholder_text="[NAME]"):
    """Preview certificate with test name"""
    try:
        # Update placeholder text
        setup_text_replacement(placeholder_text)
        
        # Load template
        template_path = load_template(template_file)
        if template_path is None:
            return f"❌ Please upload a valid Word document (.docx)"
        
        # Generate preview certificate
        preview_cert = generate_certificate(template_path, test_name)
        
        return f"βœ… Certificate will be generated with '{test_name}' replacing '{placeholder_text}'\n\nFile will be saved as PDF when sent to students."
    except Exception as e:
        return f"❌ Error: {str(e)}"

def send_email_with_certificate(recipient_email, recipient_name, cert_file_path):
    """Send certificate via email as PDF or DOCX"""
    try:
        print(f"πŸ“§ Preparing email for {recipient_name} ({recipient_email})...")
        
        # Create message
        msg = MIMEMultipart()
        msg['From'] = email_config['sender_email']
        msg['To'] = recipient_email
        msg['Subject'] = email_config.get('email_subject', 'Your Certificate of Participation')
        
        # Email body - replace {name} placeholder with actual name
        body = email_config.get('email_body', 'Dear {name},\n\nPlease find your certificate attached.').format(name=recipient_name)
        
        msg.attach(MIMEText(body, 'plain'))
        
        # Check if file exists and is a valid PDF or DOCX
        if not os.path.exists(cert_file_path):
            error_msg = f"Certificate file not found: {cert_file_path}"
            print(f"❌ {error_msg}")
            return False, error_msg
        
        print(f"πŸ“Ž Attaching: {os.path.basename(cert_file_path)}")
        
        # Determine file type and set appropriate MIME type
        file_ext = os.path.splitext(cert_file_path)[1].lower()
        
        if file_ext == '.pdf':
            mime_type = 'application/pdf'
            filename = f'Certificate_{recipient_name.replace(" ", "_")}.pdf'
        elif file_ext == '.docx':
            mime_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
            filename = f'Certificate_{recipient_name.replace(" ", "_")}.docx'
        else:
            mime_type = 'application/octet-stream'
            filename = f'Certificate_{recipient_name.replace(" ", "_")}{file_ext}'
        
        # Attach certificate file
        with open(cert_file_path, '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="{filename}"')
            part.add_header('Content-Type', mime_type)
            msg.attach(part)
        
        print(f"πŸš€ Sending email to {recipient_email}...")
        
        # Send email
        with smtplib.SMTP(email_config['smtp_server'], email_config['smtp_port']) as server:
            server.starttls()
            server.login(email_config['sender_email'], email_config['sender_password'])
            server.send_message(msg)
        
        print(f"βœ… Email sent successfully to {recipient_name}")
        return True, None
    except Exception as e:
        error_msg = f"Email error: {str(e)}"
        print(f"❌ {error_msg}")
        return False, error_msg

def send_email_with_retry(recipient_email, recipient_name, cert_file_path, max_retries=3):
    """Send email with retry logic and delay"""
    for attempt in range(max_retries):
        try:
            success, error = send_email_with_certificate(recipient_email, recipient_name, cert_file_path)
            if success:
                # Add delay to prevent being flagged as spam
                time.sleep(EMAIL_DELAY)
                return True, None
            else:
                if attempt < max_retries - 1:
                    time.sleep(5)  # Wait before retry
                    continue
                return False, error
        except Exception as e:
            if attempt < max_retries - 1:
                time.sleep(5)
                continue
            return False, str(e)
    return False, "Max retries exceeded"

def generate_single_certificate(template_path, name, placeholder_text):
    """Generate a single certificate - thread-safe"""
    try:
        # Update placeholder for this thread
        temp_dir = tempfile.mkdtemp()
        output_docx = os.path.join(temp_dir, f"Certificate_{name.replace(' ', '_')}.docx")
        
        # Replace text in Word document
        doc = replace_text_in_docx(template_path, placeholder_text, name)
        doc.save(output_docx)
        
        # Convert to PDF
        output_pdf = convert_docx_to_pdf(output_docx)
        
        return output_pdf, None
    except Exception as e:
        return None, str(e)

def process_certificates(template_file, csv_file, send_emails, placeholder_text, progress=gr.Progress()):
    """Main function to process all certificates with threading"""
    try:
        print("\n" + "="*60)
        print("πŸš€ Starting certificate processing...")
        print(f"πŸ“‹ Send emails: {send_emails}")
        print(f"πŸ”€ Placeholder text: '{placeholder_text}'")
        print("="*60 + "\n")
        
        # Validate email config if sending emails
        if send_emails:
            if not all([email_config.get('smtp_server'), email_config.get('sender_email'), email_config.get('sender_password')]):
                error_msg = "❌ Please configure email server settings first!"
                print(error_msg)
                return error_msg, None
            
            print(f"πŸ“§ Email configured:")
            print(f"  Server: {email_config['smtp_server']}:{email_config['smtp_port']}")
            print(f"  Sender: {email_config['sender_email']}")
            print(f"  Subject: {email_config.get('email_subject', 'Your Certificate')}")
        
        # Update placeholder text
        setup_text_replacement(placeholder_text)
        
        # Load template
        template_path = load_template(template_file)
        if template_path is None:
            error_msg = "❌ Could not load template file. Please upload a valid Word document (.docx)."
            print(error_msg)
            return error_msg, None
        
        print(f"βœ… Template loaded: {os.path.basename(template_path)}\n")
        
        # Read CSV
        df = pd.read_csv(csv_file)
        print(f"πŸ“Š CSV loaded: {len(df)} students found")
        print(f"   Columns: {', '.join(df.columns.tolist())}\n")
        
        # Validate CSV columns
        if 'Name' not in df.columns:
            error_msg = "❌ CSV must have a 'Name' column!"
            print(error_msg)
            return error_msg, None
        
        if send_emails and 'Email address' not in df.columns:
            error_msg = "❌ CSV must have an 'Email address' column for sending emails!"
            print(error_msg)
            return error_msg, None
        
        results = []
        total = len(df)
        successful = 0
        failed = 0
        generated_files = []
        
        print(f"πŸ“„ Phase 1: Generating {total} certificates in parallel (max {MAX_WORKERS} workers)...\n")
        
        # Phase 1: Generate all certificates in parallel with progress
        cert_futures = {}
        
        with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
            # Submit all tasks
            for idx, row in df.iterrows():
                name = row['Name']
                email = row.get('Email address', '')
                print(f"  πŸ“ Queued: {name}")
                future = executor.submit(generate_single_certificate, template_path, name, placeholder_text)
                cert_futures[future] = (name, email)
            
            print(f"\nβš™οΈ Processing {len(cert_futures)} certificates...\n")
            
            # Process completed futures with progress
            completed_count = 0
            for future in as_completed(cert_futures):
                name, email = cert_futures[future]
                completed_count += 1
                
                try:
                    cert_file, error = future.result()
                    
                    if cert_file and os.path.exists(cert_file):
                        file_ext = os.path.splitext(cert_file)[1]
                        print(f"  βœ… [{completed_count}/{total}] Generated {file_ext} for: {name}")
                        generated_files.append((cert_file, name, email))
                        if not send_emails:
                            successful += 1
                            results.append(f"βœ… {name}")
                    else:
                        print(f"  ❌ [{completed_count}/{total}] Failed for: {name} - {error if error else 'Unknown error'}")
                        failed += 1
                        results.append(f"❌ {name}: {error if error else 'Generation failed'}")
                        
                except Exception as e:
                    print(f"  ❌ [{completed_count}/{total}] Exception for: {name} - {str(e)}")
                    failed += 1
                    results.append(f"❌ {name}: {str(e)}")
                
                # Update progress
                progress(completed_count / total, desc=f"πŸ“„ Generated {completed_count}/{total} certificates")
        
        print(f"\nβœ… Phase 1 complete: {len(generated_files)} certificates generated\n")
        
        # Phase 2: Send emails sequentially with rate limiting and progress
        if send_emails and generated_files:
            email_successful = 0
            email_failed = 0
            email_total = len(generated_files)
            
            print(f"πŸ“§ Phase 2: Sending {email_total} emails sequentially...\n")
            
            for idx, (cert_file, name, email) in enumerate(generated_files):
                try:
                    print(f"  πŸ“§ [{idx+1}/{email_total}] Sending to: {name} ({email})...")
                    success, error = send_email_with_retry(email, name, cert_file)
                    
                    if success:
                        email_successful += 1
                        # Update existing result
                        for i in range(len(results)):
                            if name in results[i]:
                                results[i] = f"βœ… {name} ({email})"
                                break
                    else:
                        print(f"    ⚠️ Email failed: {error}")
                        email_failed += 1
                        # Update existing result
                        for i in range(len(results)):
                            if name in results[i]:
                                results[i] = f"❌ {name} ({email}): {error}"
                                break
                    
                    # Update progress (fix division to show correct percentage)
                    progress((idx + 1) / email_total, desc=f"πŸ“§ Sent {idx + 1}/{email_total} emails")
                    
                except Exception as e:
                    print(f"    ⚠️ Email exception: {str(e)}")
                    email_failed += 1
                    for i in range(len(results)):
                        if name in results[i]:
                            results[i] = f"❌ {name} ({email}): {str(e)}"
                            break
            
            successful = email_successful
            failed = email_failed
            
            print(f"\nβœ… Phase 2 complete: {email_successful} emails sent, {email_failed} failed\n")
        
        # Create ZIP file with all certificates
        zip_path = None
        all_cert_files = [cert_file for cert_file, _, _ in generated_files] if generated_files else []
        
        if all_cert_files:
            try:
                print(f"πŸ“¦ Creating ZIP file with {len(all_cert_files)} certificates...")
                # Create a temporary directory for the ZIP
                zip_dir = tempfile.mkdtemp()
                timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
                zip_filename = f"Certificates_{timestamp}.zip"
                zip_path = os.path.join(zip_dir, zip_filename)
                
                # Create ZIP file
                with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
                    for cert_file in all_cert_files:
                        if os.path.exists(cert_file):
                            # Add file to ZIP with just the filename (not full path)
                            zipf.write(cert_file, os.path.basename(cert_file))
                
                print(f"βœ… ZIP file created: {zip_filename} ({os.path.getsize(zip_path) / 1024:.1f} KB)\n")
            except Exception as e:
                print(f"❌ Error creating ZIP: {e}\n")
                zip_path = None
        
        # Create summary
        print("="*60)
        print(f"πŸŽ‰ Processing complete!")
        print(f"  Total: {total} | Success: {successful} | Failed: {failed}")
        print("="*60 + "\n")
        
        summary = f"Processed: {total} | Success: {successful} | Failed: {failed}\n\n"
        summary += "\n".join(results)
        
        return summary, zip_path
        
    except Exception as e:
        error_msg = f"❌ Error: {str(e)}"
        print(error_msg)
        import traceback
        print(traceback.format_exc())
        return error_msg, None

# Create Gradio Interface
with gr.Blocks(title="Certificate Generator & Email Sender", theme=gr.themes.Soft()) as app:
    gr.Markdown("""

    # πŸ“œ Certificate Generator & Email Sender

    Generate personalized certificates and optionally send them via email to participants.



    Β© 2025 @sandaruabey2025

    """)
    
    with gr.Tab("1️⃣ Email Configuration"):
        gr.Markdown("### Configure your email server settings (required only if sending emails)")
        
        with gr.Row():
            smtp_server = gr.Textbox(
                label="SMTP Server", 
                placeholder="smtp.gmail.com",
                value="smtp.gmail.com"
            )
            smtp_port = gr.Number(
                label="SMTP Port", 
                value=587
            )
        
        with gr.Row():
            sender_email = gr.Textbox(
                label="Sender Email", 
                placeholder="your-email@gmail.com"
            )
            sender_password = gr.Textbox(
                label="App Password", 
                placeholder="Your app-specific password",
                type="password"
            )
        
        email_subject = gr.Textbox(
            label="Email Subject",
            value="Your Certificate of Participation",
            placeholder="Subject line for certificate emails"
        )
        
        email_body = gr.Textbox(
            label="Email Body (use {name} for student name)",
            value="""Dear {name},



Congratulations on successfully completing the webinar on "Modern Project Management Trends and Global Opportunities for Project Management"!



Please find your certificate of participation attached to this email.



This 2 PDU Programme is accepted by PMI-USA (PDU Code: 25675RL34G).



Best regards,

Pro Consultancy International (Pvt.) Ltd

PMI Authorized Training Partner""",
            lines=12,
            placeholder="Email body template"
        )
        
        config_btn = gr.Button("πŸ’Ύ Save Configuration", variant="primary")
        config_status = gr.Textbox(label="Status", interactive=False)
        
        config_btn.click(
            fn=setup_email_server,
            inputs=[smtp_server, smtp_port, sender_email, sender_password, email_subject, email_body],
            outputs=config_status
        )
        
        gr.Markdown("""

        **Note for Gmail users:**

        1. Enable 2-Step Verification in your Google Account

        2. Generate an App Password: [Google App Passwords](https://myaccount.google.com/apppasswords)

        3. Use the generated 16-character password above

        """)
    
    with gr.Tab("2️⃣ Configure Certificate Template"):
        gr.Markdown("""

        ### Configure your Word document certificate template

        Upload your Word template and specify the placeholder text to replace

        """)
        
        template_preview = gr.File(
            label="Upload Certificate Template (Word Document .docx only)",
            file_count="single",
            type="filepath"
        )
        
        with gr.Row():
            with gr.Column():
                test_name = gr.Textbox(
                    label="Test Name",
                    value="John Doe",
                    placeholder="Enter a test name"
                )
                
                placeholder_text_input = gr.Textbox(
                    label="Placeholder Text in Word Document",
                    value="Name",
                    placeholder="e.g., Name, [NAME], Your Name",
                    info="Exact text in Word document to replace with student name"
                )
                
                preview_btn = gr.Button("πŸ” Test Configuration", variant="secondary")
            
            with gr.Column():
                position_status = gr.Textbox(label="Status", interactive=False, lines=4)
        
        preview_btn.click(
            fn=preview_with_test_name,
            inputs=[template_preview, test_name, placeholder_text_input],
            outputs=position_status
        )
        
        gr.Markdown("""

        **How to create your Word certificate:**

        1. Design your certificate in Microsoft Word

        2. Where you want the student name, type a placeholder like `[NAME]` or `Your Name`

        3. Save as .docx format

        4. Upload here and specify the exact placeholder text

        

        **Example:** If your Word doc says "This certifies that [NAME] has completed...", use `[NAME]` as placeholder

        """)
    
    with gr.Tab("3️⃣ Generate & Send Certificates"):
        gr.Markdown("### Upload certificate template and student data")
        
        with gr.Row():
            template_input = gr.File(
                label="Certificate Template (Word Document .docx)", 
                file_count="single",
                type="filepath"
            )
            csv_input = gr.File(
                label="Student Data (CSV)",
                file_count="single",
                type="filepath"
            )
        
        placeholder_text_process = gr.Textbox(
            label="Placeholder Text",
            value="Name",
            placeholder="e.g., Name, [NAME], Your Name",
            info="Exact text in Word document to replace with student name"
        )
        
        send_email_checkbox = gr.Checkbox(
            label="Send certificates via email (PDF if available, otherwise DOCX)", 
            value=False
        )
        
        gr.Markdown("""

        **⚑ Performance Features:**

        - βœ… Multi-threaded certificate generation (3 parallel workers)

        - βœ… Smart email queue with 2-second delay between sends

        - βœ… Automatic retry on email failures (3 attempts)

        - βœ… Handles large CSV files efficiently

        """)
        
        process_btn = gr.Button("πŸš€ Generate Certificates", variant="primary", size="lg")
        
        with gr.Row():
            results_output = gr.Textbox(
                label="Processing Results", 
                lines=20,
                max_lines=25
            )
            download_output = gr.File(
                label="πŸ“¦ Download All Certificates (ZIP)",
                interactive=False
            )
        
        process_btn.click(
            fn=process_certificates,
            inputs=[template_input, csv_input, send_email_checkbox, placeholder_text_process],
            outputs=[results_output, download_output]
        )
        
        gr.Markdown("""

        **πŸ’‘ Tip:** After generating certificates, you can download all of them as a ZIP file using the download button above.

        """)
    
    with gr.Tab("ℹ️ Instructions"):
        gr.Markdown("""

        ## How to Use This Application

        

        ### Step 1: Configure Email Settings (Optional)

        - Only required if you want to send certificates via email

        - For Gmail: Use an App Password, not your regular password

        

        ### Step 2: Create Word Certificate Template

        1. Design your certificate in Microsoft Word

        2. Where you want student name, type placeholder: `[NAME]`

        3. Save as .docx format

        4. Test configuration in Tab 2

        

        ### Step 3: Prepare Your CSV File

        

        Required columns:

        - `Name`: Participant's full name

        - `Email address`: Participant's email (required only if sending emails)

        

        Example CSV:

        ```

        No,Name,Email address

        1,John Doe,john@example.com

        2,Jane Smith,jane@example.com

        ```

        

        ### Step 4: Generate & Send Certificates

        - Upload Word template and CSV

        - Enter placeholder text (e.g., `[NAME]`)

        - Check "Send via email" to send as PDF

        - Click "Generate Certificates"

        

        ### Supported File Formats

        - **Certificate Template**: Word (.docx) only

        - **Student Data**: CSV

        

        ### Tips

        - Certificates are sent as PDF files via email

        - Test with 1-2 students first

        - Placeholder can appear multiple times in document

        """)
    

# Launch the app
if __name__ == "__main__":
    app.launch()