Use firm email templates, logo, and header/footer in async UI
This commit is contained in:
+58
-20
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user