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()