Spaces:
Sleeping
Sleeping
Add support for Mailjet API
Browse files- blossomtune_gradio/config.py +3 -2
- blossomtune_gradio/mail.py +133 -24
blossomtune_gradio/config.py
CHANGED
|
@@ -4,8 +4,8 @@ from blossomtune_gradio import util
|
|
| 4 |
|
| 5 |
|
| 6 |
# HF Space ID
|
| 7 |
-
SPACE_ID = os.getenv("SPACE_ID")
|
| 8 |
-
SPACE_OWNER = SPACE_ID.split("/")[0] if SPACE_ID else None
|
| 9 |
|
| 10 |
# Use persistent storage if available
|
| 11 |
DB_PATH = "/data/federation.db" if os.path.isdir("/data") else "federation.db"
|
|
@@ -16,3 +16,4 @@ SMTP_PORT = int(os.getenv("SMTP_PORT", "1025"))
|
|
| 16 |
SMTP_REQUIRE_TLS = util.strtobool(os.getenv("SMTP_REQUIRE_TLS", "false"))
|
| 17 |
SMTP_USER = os.getenv("SMTP_USER", "")
|
| 18 |
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "")
|
|
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
# HF Space ID
|
| 7 |
+
SPACE_ID = os.getenv("SPACE_ID", "ethicalabs/BlossomTune-Orchestrator")
|
| 8 |
+
SPACE_OWNER = os.getenv("SPACE_OWNER", SPACE_ID.split("/")[0] if SPACE_ID else None)
|
| 9 |
|
| 10 |
# Use persistent storage if available
|
| 11 |
DB_PATH = "/data/federation.db" if os.path.isdir("/data") else "federation.db"
|
|
|
|
| 16 |
SMTP_REQUIRE_TLS = util.strtobool(os.getenv("SMTP_REQUIRE_TLS", "false"))
|
| 17 |
SMTP_USER = os.getenv("SMTP_USER", "")
|
| 18 |
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "")
|
| 19 |
+
EMAIL_PROVIDER = os.getenv("EMAIL_PROVIDER", "smtp")
|
blossomtune_gradio/mail.py
CHANGED
|
@@ -1,17 +1,137 @@
|
|
| 1 |
import smtplib
|
| 2 |
-
|
| 3 |
from email.mime.text import MIMEText
|
| 4 |
-
|
| 5 |
|
| 6 |
from blossomtune_gradio.logs import log
|
| 7 |
from blossomtune_gradio import config as cfg
|
| 8 |
|
| 9 |
|
| 10 |
-
|
| 11 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
"""
|
| 16 |
subject = "Your BlossomTune Activation Code"
|
| 17 |
body = (
|
|
@@ -20,25 +140,14 @@ def send_activation_email(recipient_email, activation_code):
|
|
| 20 |
f"{activation_code}\n\n"
|
| 21 |
"Thank you!"
|
| 22 |
)
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
try:
|
| 29 |
-
# For local testing with a basic SMTP server like MailHog
|
| 30 |
-
with smtplib.SMTP(cfg.SMTP_SERVER, cfg.SMTP_PORT) as server:
|
| 31 |
-
# If your SMTP server requires TLS or authentication, add:
|
| 32 |
-
if cfg.SMTP_REQUIRE_TLS:
|
| 33 |
-
server.starttls()
|
| 34 |
-
server.login(cfg.SMTP_USER, cfg.SMTP_PASSWORD)
|
| 35 |
-
server.send_message(msg)
|
| 36 |
-
log(f"[Email] Activation code sent to {recipient_email}")
|
| 37 |
-
except Exception as e:
|
| 38 |
-
# FIX: return False if mail send fails.
|
| 39 |
-
log(f"[Email] CRITICAL ERROR sending to {recipient_email}: {e}")
|
| 40 |
return (
|
| 41 |
False,
|
| 42 |
-
"There was an error sending the activation email. Please contact an administrator.",
|
| 43 |
)
|
|
|
|
| 44 |
return True, ""
|
|
|
|
| 1 |
import smtplib
|
| 2 |
+
from abc import ABC, abstractmethod
|
| 3 |
from email.mime.text import MIMEText
|
| 4 |
+
import requests
|
| 5 |
|
| 6 |
from blossomtune_gradio.logs import log
|
| 7 |
from blossomtune_gradio import config as cfg
|
| 8 |
|
| 9 |
|
| 10 |
+
class EmailSender(ABC):
|
| 11 |
+
"""
|
| 12 |
+
Abstract Base Class for email sending.
|
| 13 |
+
|
| 14 |
+
This class defines the interface for all email sending implementations,
|
| 15 |
+
ensuring they have a consistent `send_email` method.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
@abstractmethod
|
| 19 |
+
def send_email(
|
| 20 |
+
self, recipient_email: str, subject: str, body: str
|
| 21 |
+
) -> tuple[bool, str]:
|
| 22 |
+
"""
|
| 23 |
+
Sends an email to the specified recipient.
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
recipient_email: The email address of the recipient.
|
| 27 |
+
subject: The subject line of the email.
|
| 28 |
+
body: The body content of the email.
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
A tuple containing a boolean success status and an error message string.
|
| 32 |
+
The error message is empty if the email was sent successfully.
|
| 33 |
+
"""
|
| 34 |
+
pass
|
| 35 |
+
|
| 36 |
|
| 37 |
+
class SMTPMailSender(EmailSender):
|
| 38 |
+
"""
|
| 39 |
+
Concrete implementation of EmailSender using standard SMTP.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
def send_email(
|
| 43 |
+
self, recipient_email: str, subject: str, body: str
|
| 44 |
+
) -> tuple[bool, str]:
|
| 45 |
+
"""
|
| 46 |
+
Sends an email using the SMTP server configured in `cfg`.
|
| 47 |
+
"""
|
| 48 |
+
msg = MIMEText(body)
|
| 49 |
+
msg["Subject"] = subject
|
| 50 |
+
msg["From"] = cfg.SMTP_SENDER
|
| 51 |
+
msg["To"] = recipient_email
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
with smtplib.SMTP(cfg.SMTP_SERVER, cfg.SMTP_PORT) as server:
|
| 55 |
+
if cfg.SMTP_REQUIRE_TLS:
|
| 56 |
+
server.starttls()
|
| 57 |
+
server.login(cfg.SMTP_USER, cfg.SMTP_PASSWORD)
|
| 58 |
+
server.send_message(msg)
|
| 59 |
+
log(f"[Email] SMTP email sent to {recipient_email}")
|
| 60 |
+
return True, ""
|
| 61 |
+
except Exception as e:
|
| 62 |
+
log(f"[Email] CRITICAL ERROR sending to {recipient_email} via SMTP: {e}")
|
| 63 |
+
return False, f"Error sending email via SMTP: {e}"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class MailjetSender(EmailSender):
|
| 67 |
+
"""
|
| 68 |
+
Concrete implementation of EmailSender using the Mailjet API.
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
def send_email(
|
| 72 |
+
self, recipient_email: str, subject: str, body: str
|
| 73 |
+
) -> tuple[bool, str]:
|
| 74 |
+
"""
|
| 75 |
+
Sends an email using the Mailjet transactional API v3.1.
|
| 76 |
+
"""
|
| 77 |
+
if not all([hasattr(cfg, "SMTP_USER"), hasattr(cfg, "SMTP_PASSWORD")]):
|
| 78 |
+
error_msg = "Mailjet API keys are not configured."
|
| 79 |
+
log(f"[Email] {error_msg}")
|
| 80 |
+
return False, error_msg
|
| 81 |
+
|
| 82 |
+
api_key = cfg.SMTP_USER
|
| 83 |
+
api_secret = cfg.SMTP_PASSWORD
|
| 84 |
+
|
| 85 |
+
url = "https://api.mailjet.com/v3.1/send"
|
| 86 |
+
data = {
|
| 87 |
+
"Messages": [
|
| 88 |
+
{
|
| 89 |
+
"From": {
|
| 90 |
+
"Email": cfg.SMTP_SENDER,
|
| 91 |
+
"Name": cfg.SMTP_SENDER.split("@")[0],
|
| 92 |
+
},
|
| 93 |
+
"To": [{"Email": recipient_email}],
|
| 94 |
+
"Subject": subject,
|
| 95 |
+
"TextPart": body,
|
| 96 |
+
}
|
| 97 |
+
]
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
try:
|
| 101 |
+
response = requests.post(url, auth=(api_key, api_secret), json=data)
|
| 102 |
+
response.raise_for_status()
|
| 103 |
+
log(
|
| 104 |
+
f"[Email] Mailjet email sent to {recipient_email}. Status: {response.status_code}"
|
| 105 |
+
)
|
| 106 |
+
return True, ""
|
| 107 |
+
except requests.exceptions.RequestException as e:
|
| 108 |
+
error_msg = f"Error sending email via Mailjet API: {e}. Response: {e.response.text if e.response else 'No response'}"
|
| 109 |
+
log(f"[Email] CRITICAL ERROR: {error_msg}")
|
| 110 |
+
return False, error_msg
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def get_email_sender() -> EmailSender:
|
| 114 |
+
"""
|
| 115 |
+
Factory function to get the correct email sender implementation.
|
| 116 |
+
|
| 117 |
+
This function reads the `EMAIL_PROVIDER` variable from the `config` module
|
| 118 |
+
and returns an appropriate EmailSender instance. Defaults to SMTP.
|
| 119 |
+
"""
|
| 120 |
+
provider = getattr(cfg, "EMAIL_PROVIDER", "smtp")
|
| 121 |
+
if provider == "mailjet":
|
| 122 |
+
return MailjetSender()
|
| 123 |
+
# Default to SMTP if the provider is not Mailjet or is missing.
|
| 124 |
+
return SMTPMailSender()
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def send_activation_email(
|
| 128 |
+
recipient_email: str, activation_code: str
|
| 129 |
+
) -> tuple[bool, str]:
|
| 130 |
+
"""
|
| 131 |
+
Sends the activation code to the user using the configured email provider.
|
| 132 |
+
|
| 133 |
+
This function uses the factory to get the correct sender and abstracts the
|
| 134 |
+
implementation details.
|
| 135 |
"""
|
| 136 |
subject = "Your BlossomTune Activation Code"
|
| 137 |
body = (
|
|
|
|
| 140 |
f"{activation_code}\n\n"
|
| 141 |
"Thank you!"
|
| 142 |
)
|
| 143 |
+
|
| 144 |
+
sender = get_email_sender()
|
| 145 |
+
success, error_message = sender.send_email(recipient_email, subject, body)
|
| 146 |
+
|
| 147 |
+
if not success:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
return (
|
| 149 |
False,
|
| 150 |
+
f"There was an error sending the activation email. Please contact an administrator. Original error: {error_message}",
|
| 151 |
)
|
| 152 |
+
|
| 153 |
return True, ""
|