This commit is contained in:
@@ -2,13 +2,57 @@
|
||||
## SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from email.message import EmailMessage
|
||||
from html import escape
|
||||
from pathlib import Path
|
||||
from smtplib import SMTP
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.database import get_connection
|
||||
from backend.app.services.secret_store import decrypt_secret, encrypt_secret
|
||||
|
||||
LOGO_PATH = Path(__file__).resolve().parents[3] / 'frontend' / 'static' / 'logo.svg'
|
||||
|
||||
|
||||
def _html_email(body_html: str) -> str:
|
||||
public_url = settings.public_url
|
||||
parsed_url = urlparse(public_url)
|
||||
public_hostname = parsed_url.hostname or public_url
|
||||
safe_public_url = escape(public_url, quote=True)
|
||||
safe_public_hostname = escape(public_hostname)
|
||||
return f'''<!doctype html>
|
||||
<html lang="en">
|
||||
<body style="margin:0;background:#1e1e2e;color:#cdd6f4;font-family:Arial,sans-serif;line-height:1.6;">
|
||||
<div style="max-width:620px;margin:32px auto;padding:0 20px;">
|
||||
<div style="background:#11111b;border:1px solid #45475a;border-radius:12px;overflow:hidden;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#181825;">
|
||||
<tr>
|
||||
<td style="padding:20px 24px;text-align:left;vertical-align:top;width:50px;">
|
||||
<img src="cid:linklog-logo" alt="LinkLog" width="50" height="50" style="display:block;width:50px;height:50px;">
|
||||
</td>
|
||||
<td style="padding:20px 0 20px 12px;text-align:left;vertical-align:top;">
|
||||
<span style="color:#cba6f7;font-family:'Asset',Georgia,serif;font-size:18px;line-height:50px;">Hello, a message from <a href="{safe_public_url}" style="color:#cba6f7;text-decoration:underline;">{safe_public_hostname}</a></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="padding:28px 32px;">{body_html}</div>
|
||||
</div>
|
||||
<p style="margin:18px 0;text-align:center;color:#a6adc8;font-size:12px;">LinkLog</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
|
||||
def _add_html_body(message: EmailMessage, html_body: str) -> None:
|
||||
message.add_alternative(_html_email(html_body), subtype='html')
|
||||
html_part = message.get_payload()[-1]
|
||||
try:
|
||||
logo = LOGO_PATH.read_bytes()
|
||||
except OSError:
|
||||
return
|
||||
html_part.add_related(logo, maintype='image', subtype='svg+xml', cid='<linklog-logo>')
|
||||
|
||||
|
||||
def get_smtp_settings() -> dict:
|
||||
values = {
|
||||
@@ -42,7 +86,8 @@ def smtp_configured(smtp_values: dict | None = None) -> bool:
|
||||
return bool(smtp['smtp_host'] and smtp['smtp_from'])
|
||||
|
||||
|
||||
def send_message(email: str, subject: str, body: str, smtp_values: dict | None = None) -> None:
|
||||
def send_message(email: str, subject: str, body: str, html_body: str | None = None,
|
||||
smtp_values: dict | None = None) -> None:
|
||||
smtp = smtp_values or get_smtp_settings()
|
||||
if not smtp_configured(smtp):
|
||||
raise RuntimeError('SMTP is not configured; set LINKLOG_SMTP_HOST and LINKLOG_SMTP_FROM')
|
||||
@@ -52,6 +97,8 @@ def send_message(email: str, subject: str, body: str, smtp_values: dict | None =
|
||||
message['From'] = smtp['smtp_from']
|
||||
message['To'] = email
|
||||
message.set_content(body)
|
||||
if html_body:
|
||||
_add_html_body(message, html_body)
|
||||
|
||||
with SMTP(smtp['smtp_host'], smtp['smtp_port'], timeout=10) as connection:
|
||||
if smtp['smtp_use_tls']:
|
||||
@@ -62,12 +109,17 @@ def send_message(email: str, subject: str, body: str, smtp_values: dict | None =
|
||||
|
||||
|
||||
def send_verification_email(email: str, username: str, verification_url: str) -> None:
|
||||
safe_username = escape(username)
|
||||
safe_url = escape(verification_url, quote=True)
|
||||
send_message(
|
||||
email,
|
||||
'Verify your LinkLog email address',
|
||||
f'Hello {username},\n\n'
|
||||
f'Verify your LinkLog email address by opening this link:\n{verification_url}\n\n'
|
||||
f'This link expires in {settings.email_verification_expiry_hours} hours.\n',
|
||||
f'<p>Hello {safe_username},</p><p>Verify your LinkLog email address:</p>'
|
||||
f'<p><a href="{safe_url}" style="display:inline-block;padding:10px 16px;background:#89b4fa;color:#11111b;text-decoration:none;border-radius:6px;">Verify email address</a></p>'
|
||||
f'<p style="color:#a6adc8;font-size:14px;">This link expires in {settings.email_verification_expiry_hours} hours.</p>',
|
||||
)
|
||||
|
||||
|
||||
@@ -76,15 +128,21 @@ def send_test_email(email: str, smtp_values: dict | None = None) -> None:
|
||||
email,
|
||||
'LinkLog SMTP test',
|
||||
'This is a test message from LinkLog. SMTP is configured correctly.\n',
|
||||
smtp_values,
|
||||
'<p>This is a test message from LinkLog.</p><p style="color:#a6adc8;">SMTP is configured correctly.</p>',
|
||||
smtp_values=smtp_values,
|
||||
)
|
||||
|
||||
|
||||
def send_password_reset_email(email: str, username: str, reset_url: str) -> None:
|
||||
safe_username = escape(username)
|
||||
safe_url = escape(reset_url, quote=True)
|
||||
send_message(
|
||||
email,
|
||||
'Reset your LinkLog password',
|
||||
f'Hello {username},\n\n'
|
||||
f'Reset your LinkLog password by opening this link:\n{reset_url}\n\n'
|
||||
f'This link expires in {settings.password_reset_expiry_hours} hours.\n',
|
||||
f'<p>Hello {safe_username},</p><p>Reset your LinkLog password:</p>'
|
||||
f'<p><a href="{safe_url}" style="display:inline-block;padding:10px 16px;background:#f38ba8;color:#11111b;text-decoration:none;border-radius:6px;">Reset password</a></p>'
|
||||
f'<p style="color:#a6adc8;font-size:14px;">This link expires in {settings.password_reset_expiry_hours} hours.</p>',
|
||||
)
|
||||
Reference in New Issue
Block a user