| """ |
| Brevo (formerly Sendinblue) email service for sending transactional emails. |
| Reference: https://developers.brevo.com/reference/sendtransacemail |
| """ |
| import os |
| import httpx |
| from typing import Optional, Dict, Any |
| from difflib import SequenceMatcher |
|
|
| BREVO_API_KEY = os.environ.get("BREVO_API_KEY", "") |
| BREVO_API_URL = "https://api.brevo.com/v3/smtp/email" |
| BREVO_SENDER_EMAIL = os.environ.get("BREVO_SENDER_EMAIL", "noreply@yourdomain.com") |
| BREVO_SENDER_NAME = os.environ.get("BREVO_SENDER_NAME", "EZOFIS AI") |
| BREVO_TRIAL_LIST_ID = int(os.environ.get("BREVO_TRIAL_LIST_ID", "5")) |
|
|
| |
| BREVO_ATTRIBUTE_MAP = { |
| "first_name": "FIRSTNAME", |
| "last_name": "LASTNAME", |
| "organization_name": "COMPANY", |
| "phone_number": "SMS", |
| "linkedin_url": "LINKEDIN", |
| "title": "JOB_TITLE", |
| "headline": "HEADLINE", |
| "organization_website": "WEBSITE", |
| "organization_address": "ADDRESS", |
| |
| "firstname": "FIRSTNAME", |
| "fname": "FIRSTNAME", |
| "given_name": "FIRSTNAME", |
| "lastname": "LASTNAME", |
| "lname": "LASTNAME", |
| "surname": "LASTNAME", |
| "family_name": "LASTNAME", |
| "company": "COMPANY", |
| "org": "COMPANY", |
| "organization": "COMPANY", |
| "phone": "SMS", |
| "mobile": "SMS", |
| "telephone": "SMS", |
| "linkedin": "LINKEDIN", |
| "linkedin_profile": "LINKEDIN", |
| "job_title": "JOB_TITLE", |
| "position": "JOB_TITLE", |
| "role": "JOB_TITLE", |
| "website": "WEBSITE", |
| "url": "WEBSITE", |
| "web": "WEBSITE", |
| "address": "ADDRESS", |
| "location": "ADDRESS", |
| } |
|
|
|
|
| def _get_brevo_attribute_name(field_name: str) -> Optional[str]: |
| """ |
| Get Brevo attribute name for a given field name using semantic matching. |
| |
| Args: |
| field_name: Field name (e.g., "first_name", "email") |
| |
| Returns: |
| Brevo attribute name (e.g., "FIRSTNAME") or None if not found |
| """ |
| |
| normalized = field_name.lower().replace("_", "").replace("-", "") |
| |
| |
| if field_name.lower() in BREVO_ATTRIBUTE_MAP: |
| return BREVO_ATTRIBUTE_MAP[field_name.lower()] |
| |
| if normalized in BREVO_ATTRIBUTE_MAP: |
| return BREVO_ATTRIBUTE_MAP[normalized] |
| |
| |
| best_match = None |
| best_score = 0.0 |
| |
| for key, value in BREVO_ATTRIBUTE_MAP.items(): |
| score = SequenceMatcher(None, normalized, key.lower()).ratio() |
| if score > best_score: |
| best_score = score |
| best_match = value |
| |
| |
| if best_score >= 0.6: |
| return best_match |
| |
| return None |
|
|
|
|
| async def send_otp_email(email: str, otp: str) -> bool: |
| """ |
| Send OTP email using Brevo transactional email API. |
| |
| Args: |
| email: Recipient email address |
| otp: One-time password code |
| |
| Returns: |
| True if email sent successfully |
| |
| Raises: |
| ValueError: If BREVO_API_KEY is not set |
| Exception: If email sending fails |
| """ |
| if not BREVO_API_KEY: |
| raise ValueError("BREVO_API_KEY environment variable is not set") |
| |
| |
| payload = { |
| "sender": { |
| "name": BREVO_SENDER_NAME, |
| "email": BREVO_SENDER_EMAIL |
| }, |
| "to": [ |
| { |
| "email": email |
| } |
| ], |
| "subject": "Your OTP Code for EZOFIS AI", |
| "htmlContent": f""" |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <style> |
| body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 0; background-color: #f4f4f4; }} |
| .container {{ max-width: 600px; margin: 20px auto; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }} |
| .content {{ padding: 40px 30px; }} |
| .content p {{ margin: 0 0 15px 0; color: #555; }} |
| .otp-box {{ background: #f8f9fa; border: 2px dashed #667eea; padding: 30px; text-align: center; margin: 30px 0; border-radius: 8px; }} |
| .otp-label {{ font-size: 14px; color: #666; margin-bottom: 10px; }} |
| .otp-code {{ font-size: 36px; font-weight: bold; color: #667eea; letter-spacing: 8px; font-family: 'Courier New', monospace; }} |
| .expiry {{ color: #888; font-size: 14px; margin-top: 20px; }} |
| .footer {{ text-align: center; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; color: #999; font-size: 12px; }} |
| .warning {{ background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; border-radius: 4px; font-size: 14px; color: #856404; }} |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="content"> |
| <p>Hello,</p> |
| <p>You requested a one-time password (OTP) to sign in to your EZOFIS account.</p> |
| <div class="otp-box"> |
| <div class="otp-label">Your OTP code is:</div> |
| <div class="otp-code">{otp}</div> |
| </div> |
| <p class="expiry">This code will expire in <strong>10 minutes</strong>.</p> |
| <div class="warning"> |
| <strong>⚠️ Security Notice:</strong> If you didn't request this code, please ignore this email. Do not share this code with anyone. |
| </div> |
| <div class="footer"> |
| <p>© EZOFIS - Agentic Intelligence Platform</p> |
| <p>This is an automated message, please do not reply.</p> |
| </div> |
| </div> |
| </div> |
| </body> |
| </html> |
| """, |
| "textContent": f""" |
| Your OTP Code for EZOFIS AI |
| |
| Hello, |
| |
| You requested a one-time password (OTP) to sign in to your EZOFIS account. |
| |
| Your OTP code is: {otp} |
| |
| This code will expire in 10 minutes. |
| |
| ⚠️ Security Notice: If you didn't request this code, please ignore this email. Do not share this code with anyone. |
| |
| © EZOFIS - Agentic Intelligence Platform |
| This is an automated message, please do not reply. |
| """ |
| } |
| |
| headers = { |
| "accept": "application/json", |
| "api-key": BREVO_API_KEY, |
| "content-type": "application/json" |
| } |
| |
| try: |
| async with httpx.AsyncClient(timeout=30.0) as client: |
| response = await client.post(BREVO_API_URL, json=payload, headers=headers) |
| response.raise_for_status() |
| |
| result = response.json() |
| message_id = result.get('messageId', 'N/A') |
| print(f"[INFO] Brevo email sent successfully to {email}. Message ID: {message_id}") |
| return True |
| except httpx.HTTPStatusError as e: |
| error_detail = {} |
| try: |
| error_detail = e.response.json() if e.response else {} |
| except: |
| error_detail = {"message": str(e)} |
| |
| error_msg = error_detail.get('message', f'HTTP {e.response.status_code}' if e.response else 'Unknown error') |
| print(f"[ERROR] Brevo API error: {e.response.status_code if e.response else 'N/A'} - {error_msg}") |
| raise Exception(f"Failed to send email via Brevo: {error_msg}") |
| except httpx.TimeoutException: |
| print(f"[ERROR] Brevo API request timed out") |
| raise Exception("Email service timeout. Please try again.") |
| except Exception as e: |
| print(f"[ERROR] Brevo email sending failed: {str(e)}") |
| raise Exception(f"Failed to send email: {str(e)}") |
|
|
|
|
| async def send_share_email(recipient_email: str, sender_email: str, share_link: str, sender_name: str = None) -> bool: |
| """ |
| Send share email using Brevo transactional email API. |
| |
| Args: |
| recipient_email: Recipient email address |
| sender_email: Sender email address |
| share_link: Share link URL |
| sender_name: Sender's display name (optional, falls back to email if not provided) |
| |
| Returns: |
| True if email sent successfully |
| |
| Raises: |
| ValueError: If BREVO_API_KEY is not set |
| Exception: If email sending fails |
| """ |
| if not BREVO_API_KEY: |
| raise ValueError("BREVO_API_KEY environment variable is not set") |
| |
| |
| base_url = os.environ.get("VITE_API_BASE_URL", "https://seth0330-ezofisocr.hf.space") |
| |
| |
| |
| |
| if sender_name and sender_name.strip(): |
| |
| sender_display = sender_name.strip() |
| print(f"[INFO] Using user's name from database: {sender_display}") |
| else: |
| |
| email_name = sender_email.split('@')[0] |
| |
| if '.' in email_name: |
| parts = email_name.split('.') |
| sender_display = ' '.join(part.capitalize() for part in parts) |
| elif '_' in email_name: |
| parts = email_name.split('_') |
| sender_display = ' '.join(part.capitalize() for part in parts) |
| else: |
| |
| sender_display = email_name.capitalize() |
| print(f"[INFO] Extracted name from email: {sender_display} (from {sender_email})") |
| |
| |
| |
| |
| payload = { |
| "sender": { |
| "name": BREVO_SENDER_NAME, |
| "email": BREVO_SENDER_EMAIL |
| }, |
| "to": [ |
| { |
| "email": recipient_email |
| } |
| ], |
| "subject": f"{sender_display} shared a document extraction with you", |
| "htmlContent": f""" |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <style> |
| body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 0; background-color: #f4f4f4; }} |
| .container {{ max-width: 600px; margin: 20px auto; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }} |
| .content {{ padding: 40px 30px; }} |
| .content p {{ margin: 0 0 15px 0; color: #555; }} |
| .share-box {{ background: #f8f9fa; border: 2px solid #667eea; padding: 30px; text-align: center; margin: 30px 0; border-radius: 8px; }} |
| .share-button {{ display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff !important; padding: 15px 30px; text-decoration: none; border-radius: 8px; font-weight: 600; margin-top: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; }} |
| .share-button:hover {{ color: #ffffff !important; }} |
| .footer {{ text-align: center; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; color: #999; font-size: 12px; }} |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="content"> |
| <p>Hello,</p> |
| <p><strong>{sender_display}</strong> wants you to take a look at a document extraction output.</p> |
| <div class="share-box"> |
| <p style="margin-bottom: 20px; color: #666;">Click the button below to view the shared extraction:</p> |
| <a href="{share_link}" class="share-button">View Shared Extraction</a> |
| </div> |
| <p style="color: #888; font-size: 14px;">You'll need to sign in to your EZOFIS account to view this extraction. If you don't have an account, you can create one using the link above.</p> |
| <div class="footer"> |
| <p>© EZOFIS - Agentic Intelligence Platform</p> |
| <p>This is an automated message, please do not reply.</p> |
| </div> |
| </div> |
| </div> |
| </body> |
| </html> |
| """, |
| "textContent": f""" |
| {sender_display} shared a document extraction with you |
| |
| Hello, |
| |
| {sender_display} wants you to take a look at a document extraction output. |
| |
| View the shared extraction: {share_link} |
| |
| You'll need to sign in to your EZOFIS account to view this extraction. If you don't have an account, you can create one using the link above. |
| |
| © EZOFIS - Agentic Intelligence Platform |
| This is an automated message, please do not reply. |
| """ |
| } |
| |
| headers = { |
| "accept": "application/json", |
| "api-key": BREVO_API_KEY, |
| "content-type": "application/json" |
| } |
| |
| try: |
| async with httpx.AsyncClient(timeout=30.0) as client: |
| response = await client.post(BREVO_API_URL, json=payload, headers=headers) |
| response.raise_for_status() |
| |
| result = response.json() |
| message_id = result.get('messageId', 'N/A') |
| print(f"[INFO] Brevo share email sent successfully to {recipient_email}. Message ID: {message_id}") |
| return True |
| except httpx.HTTPStatusError as e: |
| error_detail = {} |
| try: |
| error_detail = e.response.json() if e.response else {} |
| except: |
| error_detail = {"message": str(e)} |
| |
| error_msg = error_detail.get('message', f'HTTP {e.response.status_code}' if e.response else 'Unknown error') |
| print(f"[ERROR] Brevo API error: {e.response.status_code if e.response else 'N/A'} - {error_msg}") |
| raise Exception(f"Failed to send email via Brevo: {error_msg}") |
| except httpx.TimeoutException: |
| print(f"[ERROR] Brevo API request timed out") |
| raise Exception("Email service timeout. Please try again.") |
| except Exception as e: |
| print(f"[ERROR] Brevo email sending failed: {str(e)}") |
| raise Exception(f"Failed to send email: {str(e)}") |
|
|
|
|
| async def create_brevo_contact( |
| email: str, |
| first_name: Optional[str] = None, |
| last_name: Optional[str] = None, |
| organization_name: Optional[str] = None, |
| phone_number: Optional[str] = None, |
| linkedin_url: Optional[str] = None, |
| title: Optional[str] = None, |
| headline: Optional[str] = None, |
| organization_website: Optional[str] = None, |
| organization_address: Optional[str] = None, |
| list_id: Optional[int] = None |
| ) -> bool: |
| """ |
| Create a contact in Brevo and optionally add to a list. |
| |
| Args: |
| email: Contact email address (required) |
| first_name: Contact first name |
| last_name: Contact last name |
| organization_name: Organization name |
| phone_number: Phone number |
| linkedin_url: LinkedIn profile URL |
| title: Job title |
| headline: Professional headline |
| organization_website: Company website |
| organization_address: Company address |
| list_id: ID of the list to add contact to (e.g., 5 for "VRP Trials") |
| |
| Returns: |
| True if contact created successfully, False otherwise |
| """ |
| if not BREVO_API_KEY: |
| print("[WARNING] BREVO_API_KEY not set, skipping Brevo contact creation") |
| return False |
| |
| |
| attributes = {} |
| |
| |
| field_mappings = { |
| "first_name": first_name, |
| "last_name": last_name, |
| "organization_name": organization_name, |
| "phone_number": phone_number, |
| "linkedin_url": linkedin_url, |
| "title": title, |
| "headline": headline, |
| "organization_website": organization_website, |
| "organization_address": organization_address, |
| } |
| |
| for field_name, field_value in field_mappings.items(): |
| if field_value: |
| brevo_attr = _get_brevo_attribute_name(field_name) |
| if brevo_attr: |
| attributes[brevo_attr] = str(field_value).strip() |
| print(f"[DEBUG] Mapped '{field_name}' ({field_value}) to Brevo attribute '{brevo_attr}'") |
| else: |
| print(f"[DEBUG] No Brevo attribute mapping found for '{field_name}'") |
| else: |
| print(f"[DEBUG] Skipping '{field_name}' - value is empty/None") |
| |
| print(f"[DEBUG] Final Brevo attributes to send: {attributes}") |
| |
| |
| contact_data = { |
| "email": email.lower(), |
| "updateEnabled": True |
| } |
| |
| if attributes: |
| contact_data["attributes"] = attributes |
| |
| |
| if list_id: |
| contact_data["listIds"] = [list_id] |
| |
| headers = { |
| "accept": "application/json", |
| "api-key": BREVO_API_KEY, |
| "content-type": "application/json" |
| } |
| |
| try: |
| async with httpx.AsyncClient(timeout=30.0) as client: |
| response = await client.post( |
| "https://api.brevo.com/v3/contacts", |
| json=contact_data, |
| headers=headers |
| ) |
| |
| if response.status_code in [200, 201, 204]: |
| print(f"[INFO] Successfully created Brevo contact: {email}" + |
| (f" and added to list {list_id}" if list_id else "")) |
| return True |
| elif response.status_code == 400: |
| |
| try: |
| error_data = response.json() |
| if "already exists" in str(error_data).lower(): |
| print(f"[INFO] Contact {email} already exists in Brevo, updating...") |
| |
| update_response = await client.put( |
| f"https://api.brevo.com/v3/contacts/{email.lower()}", |
| json=contact_data, |
| headers=headers |
| ) |
| if update_response.status_code in [200, 204]: |
| print(f"[INFO] Successfully updated Brevo contact: {email}" + |
| (f" and added to list {list_id}" if list_id else "")) |
| return True |
| except: |
| pass |
| |
| error_data = response.text |
| print(f"[ERROR] Failed to create Brevo contact: {response.status_code} - {error_data}") |
| return False |
| else: |
| error_data = response.text |
| print(f"[ERROR] Failed to create Brevo contact: {response.status_code} - {error_data}") |
| return False |
| |
| except httpx.HTTPStatusError as e: |
| print(f"[ERROR] Brevo API HTTP error: {e.response.status_code} - {e.response.text}") |
| return False |
| except Exception as e: |
| print(f"[ERROR] Failed to create Brevo contact: {str(e)}") |
| return False |
|
|
|
|