email box empty styling

This commit is contained in:
debu-2 2026-10-05 21:34:12 +05:30
parent 1ee8a58117
commit 5cb49f4cb1
19 changed files with 547 additions and 67 deletions

View file

@ -29,6 +29,7 @@ class Settings(BaseSettings):
# Outbound via self-hosted Postfix (Docker service `smtp` by default)
smtp_host: str = "smtp"
smtp_port: int = 25
smtp_client_hostname: str = "backend.gentic.email"
smtp_public_host: str = "mail.gentic.email"
smtp_username: str = ""
smtp_password: str = ""

View file

@ -1,3 +1,4 @@
import logging
import re
from datetime import date, datetime, timedelta, timezone
from uuid import UUID
@ -24,6 +25,9 @@ from app.schemas import (
PasswordChangeRequest,
)
from app.services.member_invitations import get_valid_invitation
from app.services.welcome_email import send_welcome_email
logger = logging.getLogger(__name__)
from app.services.domain_policy import assert_company_domain_allowed
from app.services.domain_provisioning import account_is_domain_owner
from app.services.domain_provisioning import (
@ -292,6 +296,10 @@ async def accept_invite(
invitation.used_at = datetime.now(timezone.utc)
await session.commit()
await session.refresh(account)
try:
await send_welcome_email(session, account)
except Exception as error:
logger.warning("Welcome email could not be sent: %s", error)
return AuthResponse(
access_token=create_access_token(account),
account=await account_read(session, account),

View file

@ -159,3 +159,72 @@ def build_employee_invitation_email(
</html>"""
return subject, body_text, body_html
def build_welcome_email(
*,
first_name: str,
work_email: str,
organization: str | None,
login_url: str,
is_company_account: bool,
) -> tuple[str, str, str]:
name = first_name.strip() or "there"
subject = "Welcome to AGMail"
org_line = (
f"Your {work_email} mailbox is ready."
if is_company_account
else f"Your {work_email} address is active."
)
admin_note = (
"Your organization administrator manages this account."
if is_company_account and organization
else ""
)
body_text = (
f"Hello {name},\n\n"
f"Welcome to {BRAND_NAME}. {org_line}\n\n"
f"What you can do now:\n"
f"- Compose your first email from the inbox\n"
f"- Ask AG Agent to summarize threads and surface action items\n"
f"- Review account and security settings\n\n"
f"Sign in: {login_url}\n\n"
f"{admin_note}\n\n"
f"—\n"
f"AGMail Team\n"
f"{BRAND_NAME}"
).strip()
safe_name = _escape(name)
safe_email = _escape(work_email)
safe_login = _escape(login_url)
safe_org = _escape(organization or "")
safe_admin = _escape(admin_note)
body_html = f"""<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8" /><title>{_escape(subject)}</title></head>
<body style="margin:0;padding:32px 16px;background:#fffdfb;color:#080821;font-family:'DM Sans',sans-serif;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
<tr><td align="center">
<table role="presentation" width="100%" style="max-width:600px;background:#fff;border:1px solid #eee9e5;border-radius:20px;">
<tr><td style="padding:28px 32px;">
<p style="margin:0 0 12px;font-size:12px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:#ff6a52;">Welcome</p>
<h1 style="margin:0 0 16px;font-size:24px;">Welcome to AGMail</h1>
<p style="margin:0 0 16px;font-size:15px;line-height:1.7;color:#67677a;">Hello {safe_name}, your mailbox <strong>{safe_email}</strong> is ready.</p>
{"<p style='margin:0 0 16px;font-size:14px;color:#67677a;'>Organization: <strong>" + safe_org + "</strong></p>" if organization else ""}
<ul style="margin:0 0 20px;padding-left:20px;color:#67677a;font-size:14px;line-height:1.7;">
<li>Compose email from your inbox</li>
<li>Use AG Agent for summaries and action items</li>
<li>Review security settings after sign-in</li>
</ul>
<a href="{safe_login}" style="display:inline-block;padding:12px 18px;border-radius:10px;background:#080821;color:#fff;text-decoration:none;font-weight:700;">Open AGMail</a>
{"<p style='margin:18px 0 0;font-size:12px;color:#8d8792;'>" + safe_admin + "</p>" if admin_note else ""}
</td></tr>
</table>
</td></tr>
</table>
</body></html>"""
return subject, body_text, body_html

View file

@ -76,6 +76,7 @@ async def send_via_smtp(
recipients=recipients,
hostname=settings.smtp_host,
port=settings.smtp_port,
local_hostname=settings.smtp_client_hostname,
username=settings.smtp_username or None,
password=settings.smtp_password or None,
start_tls=settings.smtp_use_tls,

View file

@ -0,0 +1,44 @@
"""Welcome message delivered to a newly activated mailbox."""
from __future__ import annotations
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import get_settings
from app.models import Account
from app.services.domain_provisioning import get_domain_by_name
from app.services.email_templates import BRAND_NAME, build_welcome_email
from app.services.mail_sender import deliver_email
logger = logging.getLogger(__name__)
NOTIFICATIONS_FROM_LOCAL = "notifications"
async def send_welcome_email(session: AsyncSession, account: Account) -> None:
settings = get_settings()
platform_domain = await get_domain_by_name(session, settings.platform_domain)
if platform_domain is None:
raise ValueError("Platform mail domain is not configured.")
login_url = f"https://{settings.app_public_host}/login"
organization = account.company_domain if account.account_type == "company" else None
subject, body_text, body_html = build_welcome_email(
first_name=account.first_name,
work_email=account.email,
organization=organization,
login_url=login_url,
is_company_account=account.account_type == "company",
)
sender = f"AGMail Team <{NOTIFICATIONS_FROM_LOCAL}@{settings.platform_domain}>"
await deliver_email(
sender=sender,
recipients=[account.email],
cc=[],
bcc=[],
subject=subject,
body_text=body_text,
body_html=body_html,
domain=platform_domain,
)

View file

@ -1,4 +1,7 @@
from app.services.email_templates import build_employee_invitation_email
from app.services.email_templates import (
build_employee_invitation_email,
build_welcome_email,
)
def test_employee_invitation_uses_token_link_not_password():
@ -22,3 +25,17 @@ def test_employee_invitation_uses_token_link_not_password():
assert "test-token" in body_html
assert "Initial password" not in body_html
assert "unsubscribe" not in body_html.lower()
def test_welcome_email_is_useful_not_marketing():
subject, body_text, body_html = build_welcome_email(
first_name="Anurag",
work_email="anurag@road.rentals",
organization="road.rentals",
login_url="https://gentic.email/login",
is_company_account=True,
)
assert subject == "Welcome to AGMail"
assert "AG Agent" in body_text
assert "anurag@road.rentals" in body_html
assert "unsubscribe" not in body_text.lower()

View file

@ -1,7 +1,7 @@
from types import SimpleNamespace
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from app.services.mail_sender import build_message, sign_message
from app.services.mail_sender import build_message, send_via_smtp, sign_message
def test_dkim_sign_uses_selector_before_domain():
@ -27,3 +27,17 @@ def test_dkim_sign_uses_selector_before_domain():
assert signer.call_args.args[1] == b"agmail"
assert signer.call_args.args[2] == b"ritoesis.com"
async def test_send_via_smtp_uses_configured_client_hostname():
with patch(
"app.services.mail_sender.aiosmtplib.send",
new_callable=AsyncMock,
) as send_mock:
await send_via_smtp(
b"raw",
sender="noreply@gentic.email",
recipients=["user@example.com"],
)
assert send_mock.await_args.kwargs["local_hostname"] == "backend.gentic.email"