Use firm email templates, logo, and header/footer in async UI
Mirror and run GitLab CI / build (push) Has been cancelled
Ruff / ruff (push) Has been cancelled

This commit is contained in:
admin
2026-06-14 14:54:39 +00:00
parent 2803c81b44
commit 85cdd9216a
3 changed files with 143 additions and 37 deletions
+58 -20
View File
@@ -3,6 +3,7 @@ Email sender module for ScrAIbe.
Sends transcription outputs (TXT, JSON, etc.) via SMTP.
All credentials are configured via environment variables.
Supports both plain text and HTML email bodies.
"""
import os
@@ -51,20 +52,47 @@ def get_email_config():
}
def load_template(template_name: str, **kwargs) -> str:
"""
Load an HTML email template from misc/ and render placeholders.
Expects files like:
/app/src/misc/upload_notification_template.html
/app/src/misc/success_template.html
/app/src/misc/error_notification_template.html
"""
base = os.getenv("SCRAIBE_TEMPLATES_DIR", "/app/src/misc")
path = os.path.join(base, template_name)
if not os.path.exists(path):
raise EmailError(f"Email template not found: {path}")
with open(path, "r", encoding="utf-8") as f:
template = f.read()
# Replace {placeholder} style variables safely
try:
return template.format(**kwargs)
except KeyError as e:
raise EmailError(f"Missing template variable: {e}")
def send_email(
to: str,
subject: str,
body: str,
html: Optional[str],
attachments: List[str],
cc: Optional[str] = None,
) -> bool:
"""
Send an email with optional file attachments.
Send an email with optional HTML body and file attachments.
Args:
to: Comma-separated list of recipient email addresses.
subject: Email subject.
body: Email body (plain text).
html: Email body (HTML), or None.
attachments: List of file paths to attach.
cc: Comma-separated list of CC email addresses (optional).
@@ -88,34 +116,44 @@ def send_email(
raise EmailError("No valid 'To' email addresses provided.")
# Build message
msg = MIMEMultipart()
msg = MIMEMultipart("alternative")
msg["From"] = cfg["from_address"]
msg["To"] = ", ".join(to_list)
if cc_list:
msg["Cc"] = ", ".join(cc_list)
msg["Subject"] = subject
# Attach plain text
msg.attach(MIMEText(body, "plain"))
# Attach files
for file_path in attachments:
if not os.path.isfile(file_path):
logger.warning("Attachment file not found, skipping: %s", file_path)
continue
# Attach HTML if provided
if html:
msg.attach(MIMEText(html, "html"))
try:
with open(file_path, "rb") as f:
part = MIMEBase("application", "octet-stream")
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header(
"Content-Disposition",
"attachment",
filename=os.path.basename(file_path),
)
msg.attach(part)
except Exception as e:
logger.warning("Failed to attach file %s: %s", file_path, e)
# Attach files in a separate multipart/mixed part
if attachments:
mixed = MIMEMultipart("mixed")
mixed.attach(msg)
msg = mixed
for file_path in attachments:
if not os.path.isfile(file_path):
logger.warning("Attachment file not found, skipping: %s", file_path)
continue
try:
with open(file_path, "rb") as f:
part = MIMEBase("application", "octet-stream")
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header(
"Content-Disposition",
"attachment",
filename=os.path.basename(file_path),
)
msg.attach(part)
except Exception as e:
logger.warning("Failed to attach file %s: %s", file_path, e)
# Connect and send
try:
+52 -6
View File
@@ -11,10 +11,13 @@ from datetime import datetime
from .celery_app import celery_app
from .autotranscript import Scraibe
from .misc import setup_logging
from .email_sender import send_email, EmailError
from .email_sender import send_email, EmailError, load_template
logger = logging.getLogger("scraibe.tasks")
# Contact email used in templates; can be overridden via env
CONTACT_EMAIL = os.getenv("EMAIL_CONTACT_ADDRESS", "support@example.com")
def get_queue_position(task_id: str) -> int:
"""
@@ -38,9 +41,11 @@ def get_queue_position(task_id: str) -> int:
def send_initial_email(to: str, queue_pos: int):
"""
Send initial confirmation email with queue position.
Send initial confirmation email with queue position using HTML template.
"""
subject = "ScrAIbe: Your transcription request has been received"
# Build plain-text fallback
body = (
"Hello,\n\n"
"We have received your audio file for transcription.\n"
@@ -55,11 +60,24 @@ def send_initial_email(to: str, queue_pos: int):
"\n"
"You will receive an email with your transcript (and summary, if requested) "
"once processing is complete.\n\n"
"If you have any questions, contact us at "
f"{CONTACT_EMAIL}.\n\n"
"This is an automated message from ScrAIbe.\n"
)
# Build HTML using template
html = None
try:
send_email(to=to, subject=subject, body=body, attachments=[])
html = load_template(
"upload_notification_template.html",
queue_position=str(queue_pos) if queue_pos > 0 else "the queue",
contact_email=CONTACT_EMAIL,
)
except EmailError as e:
logger.warning("Failed to render upload notification template: %s", e)
try:
send_email(to=to, subject=subject, body=body, html=html, attachments=[])
logger.info("Initial confirmation email sent to %s", to)
except EmailError as e:
logger.error("Failed to send initial email to %s: %s", to, e)
@@ -73,10 +91,11 @@ def send_success_email(
task_id: str,
):
"""
Send final email with transcript and attachments.
Send final email with transcript and attachments using HTML template.
"""
subject = "ScrAIbe: Your transcript is ready"
# Build plain-text fallback
body = (
"Hello,\n\n"
"Your transcription is ready.\n\n"
@@ -94,14 +113,27 @@ def send_success_email(
body += (
"\n"
"Job ID: " + str(task_id) + "\n\n"
"If you have any questions, contact us at "
f"{CONTACT_EMAIL}.\n\n"
"This is an automated message from ScrAIbe.\n"
)
# Build HTML using template
html = None
try:
html = load_template(
"success_template.html",
contact_email=CONTACT_EMAIL,
)
except EmailError as e:
logger.warning("Failed to render success template: %s", e)
try:
send_email(
to=to,
subject=subject,
body=body,
html=html,
attachments=attachments,
)
logger.info("Success email sent to %s for job %s", to, task_id)
@@ -111,21 +143,35 @@ def send_success_email(
def send_error_email(to: str, error_message: str, task_id: str):
"""
Send error notification email.
Send error notification email using HTML template.
"""
subject = "ScrAIbe: Error with your transcription request"
# Build plain-text fallback
body = (
"Hello,\n\n"
"We encountered an error while processing your transcription request.\n\n"
f"Details: {error_message}\n\n"
"Job ID: " + str(task_id) + "\n\n"
"Please contact your administrator if the problem persists.\n\n"
"If you have any questions, contact us at "
f"{CONTACT_EMAIL}.\n\n"
"This is an automated message from ScrAIbe.\n"
)
# Build HTML using template
html = None
try:
send_email(to=to, subject=subject, body=body, attachments=[])
html = load_template(
"error_notification_template.html",
exception=str(error_message),
contact_email=CONTACT_EMAIL,
)
except EmailError as e:
logger.warning("Failed to render error template: %s", e)
try:
send_email(to=to, subject=subject, body=body, html=html, attachments=[])
logger.info("Error email sent to %s for job %s", to, task_id)
except EmailError as e:
logger.error("Failed to send error email to %s for job %s: %s", to, task_id, e)
+33 -11
View File
@@ -45,6 +45,20 @@ def load_config():
return config
def load_html_template(path: str, **kwargs) -> str:
"""
Load an HTML template and fill placeholders.
"""
if not os.path.exists(path):
return ""
with open(path, "r", encoding="utf-8") as f:
template = f.read()
try:
return template.format(**kwargs)
except KeyError:
return template
def create_app():
"""
Create and launch the Gradio Web GUI (async mode).
@@ -65,20 +79,28 @@ def create_app():
upload_dir = os.getenv("SCRAIBE_UPLOAD_DIR", "/tmp/scraibe_uploads")
os.makedirs(upload_dir, exist_ok=True)
# Load header/footer HTML if present
# Paths for assets
header_path = layout_cfg.get("header", "/app/src/misc/header.html")
footer_path = layout_cfg.get("footer", "/app/src/misc/footer.html")
logo_path = layout_cfg.get("logo", "/app/src/misc/logo.png")
# Prepare header HTML with logo
header_html = ""
if os.path.exists(header_path):
header_html = load_html_template(
header_path,
header_logo_url="https://www.apstrom.de/",
header_logo_src=logo_path if os.path.exists(logo_path) else "",
)
# Prepare footer HTML
footer_html = ""
if header_path and os.path.exists(header_path):
with open(header_path, "r", encoding="utf-8") as f:
header_html = f.read()
if footer_path and os.path.exists(footer_path):
with open(footer_path, "r", encoding="utf-8") as f:
footer_html = f.read()
if os.path.exists(footer_path):
version = os.getenv("SCRABIE_VERSION", "0.1.1.dev")
footer_html = load_html_template(
footer_path,
footer_scraibe_webui_version=version,
)
# Build Gradio interface
with gr.Blocks(
@@ -225,12 +247,12 @@ def create_app():
# Launch options
server_name = launch_cfg.get("server_name", os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"))
server_port = launch_cfg.get("server_port", 7860)
favicon_path = launch_cfg.get("favicon_path", "/app/src/misc/logo.png")
favicon_path = logo_path if os.path.exists(logo_path) else None
app.launch(
server_name=str(server_name),
server_port=int(server_port),
favicon_path=favicon_path if os.path.exists(favicon_path) else None,
favicon_path=favicon_path,
css="body { font-family: Arial, sans-serif; }",
)