From 65bb73f30ba266339987fec397a08215d6bbf19d Mon Sep 17 00:00:00 2001 From: debu-2 Date: Sun, 4 Oct 2026 21:02:19 +0530 Subject: [PATCH] agentic email invite html --- backend/app/services/email_templates.py | 219 ++++++++++++++++++ backend/app/services/mail_sender.py | 5 + backend/app/services/org_invitations.py | 43 ++-- backend/tests/test_email_templates.py | 32 +++ frontend/src/components/AddEmployeeForm.jsx | 4 +- .../src/components/ChangePasswordModal.jsx | 2 +- 6 files changed, 273 insertions(+), 32 deletions(-) create mode 100644 backend/app/services/email_templates.py create mode 100644 backend/tests/test_email_templates.py diff --git a/backend/app/services/email_templates.py b/backend/app/services/email_templates.py new file mode 100644 index 0000000..f05896f --- /dev/null +++ b/backend/app/services/email_templates.py @@ -0,0 +1,219 @@ +"""HTML and plain-text templates for transactional email.""" + +from __future__ import annotations + +import html + +BRAND_NAME = "aGentic Email" +BRAND_HANDLE = "@gentic.email" + + +def _escape(value: str) -> str: + return html.escape(value, quote=True) + + +def build_employee_invitation_email( + *, + member_first_name: str, + member_email: str, + manager_name: str, + manager_email: str, + company_domain: str, + login_url: str, + initial_password: str, + force_password_change: bool, +) -> tuple[str, str, str]: + first_name = member_first_name.strip() or "there" + organization = company_domain.strip() + manager = manager_name.strip() or "Your administrator" + manager_address = manager_email.strip() + + security_note = ( + "For your security, you will be asked to create a new password " + "immediately after your first sign-in." + if force_password_change + else ( + "We recommend updating your password after your first sign-in " + "from your account settings." + ) + ) + + subject = ( + f"Account invitation: {manager} added you to {organization} on {BRAND_NAME}" + ) + + unexpected_contact = ( + f"If you were not expecting this invitation, please contact " + f"{manager} at {manager_address}." + if manager_address + else "If you were not expecting this invitation, please contact your administrator." + ) + + body_text = ( + f"Hello {first_name},\n\n" + f"{manager} has created a work email account for you on {BRAND_NAME}, " + f"the email platform used by {organization}.\n\n" + f"This automated notification was sent to the personal email address " + f"your administrator provided. It contains the sign-in details for " + f"your new organization account.\n\n" + f"Organization: {organization}\n" + f"Work email address: {member_email}\n" + f"Sign-in page: {login_url}\n\n" + f"Sign-in credentials\n" + f" Email: {member_email}\n" + f" Initial password: {initial_password}\n\n" + f"{security_note}\n\n" + f"{unexpected_contact}\n\n" + f"—\n" + f"{BRAND_NAME}\n" + f"{login_url.rsplit('/login', 1)[0] or login_url}\n" + f"This is an account notification sent on behalf of {organization}." + ) + + preheader = ( + f"{manager} added you to {organization}. " + f"Your work email account is ready — review your sign-in details." + ) + + safe_first = _escape(first_name) + safe_manager = _escape(manager) + safe_org = _escape(organization) + safe_email = _escape(member_email) + safe_password = _escape(initial_password) + safe_login = _escape(login_url) + safe_manager_email = _escape(manager_address) + safe_preheader = _escape(preheader) + safe_security = _escape(security_note) + safe_unexpected = _escape(unexpected_contact) + + body_html = f""" + + + + + + + {_escape(subject)} + + +
+ {safe_preheader} +
+ + + + +
+ + + + + + + +
+ + + + + +
+ + + gentic.email +
+
+ + + + + + + + + + +
+

Organization invitation

+

+ You have been added to
+ {safe_org} +

+
+

+ Hello {safe_first}, +

+

+ {safe_manager} has created a work email + account for you on {BRAND_NAME}, + the email platform used by {safe_org} + for business communication. +

+

+ This message was sent to the personal email address your administrator + provided. Use the details below to access your new organization inbox. +

+ + + + +
+

Account details

+ + + + + + + + + + + + + +
Organization{safe_org}
Work email{safe_email}
Initial password{safe_password}
+
+ + + + +
+ + Sign in to {BRAND_NAME} + +
+

+ {safe_security} +

+

+ {safe_unexpected} +

+
+

+ {BRAND_NAME} · {BRAND_HANDLE} +

+

+ This is an automated account notification sent on behalf of {safe_org}. + {" Reply to " + safe_manager_email + " if you need help." if manager_address else ""} +

+
+
+
+ +""" + + return subject, body_text, body_html diff --git a/backend/app/services/mail_sender.py b/backend/app/services/mail_sender.py index b5f1716..16e44ff 100644 --- a/backend/app/services/mail_sender.py +++ b/backend/app/services/mail_sender.py @@ -19,12 +19,15 @@ def build_message( subject: str, body_text: str, body_html: str | None, + reply_to: str | None = None, ) -> EmailMessage: message = EmailMessage(policy=SMTP_POLICY) message["From"] = sender message["To"] = ", ".join(recipients) if cc: message["Cc"] = ", ".join(cc) + if reply_to: + message["Reply-To"] = reply_to message["Subject"] = subject message["Date"] = email.utils.formatdate(localtime=True) message["Message-ID"] = email.utils.make_msgid(domain=sender.split("@")[-1]) @@ -90,6 +93,7 @@ async def deliver_email( body_text: str, body_html: str | None, domain: MailDomain, + reply_to: str | None = None, ) -> None: message = build_message( sender=sender, @@ -98,6 +102,7 @@ async def deliver_email( subject=subject, body_text=body_text, body_html=body_html, + reply_to=reply_to, ) signed = sign_message(message, domain) await send_via_smtp( diff --git a/backend/app/services/org_invitations.py b/backend/app/services/org_invitations.py index 4a2cbab..a669ddd 100644 --- a/backend/app/services/org_invitations.py +++ b/backend/app/services/org_invitations.py @@ -5,8 +5,9 @@ from __future__ import annotations from sqlalchemy.ext.asyncio import AsyncSession from app.config import get_settings -from app.models import Account, MailDomain +from app.models import Account from app.services.domain_provisioning import get_domain_by_name +from app.services.email_templates import BRAND_NAME, build_employee_invitation_email from app.services.mail_sender import deliver_email @@ -30,42 +31,26 @@ async def send_employee_invitation( manager_name = f"{manager.first_name} {manager.last_name}".strip() company = member.company_domain or settings.platform_domain - change_note = ( - "You will be asked to choose a new password immediately after signing in." - if force_password_change - else "You can change your password later from account settings." + subject, body_text, body_html = build_employee_invitation_email( + member_first_name=member.first_name, + member_email=member.email, + manager_name=manager_name, + manager_email=manager.email, + company_domain=company, + login_url=login_url, + initial_password=initial_password, + force_password_change=force_password_change, ) - body_text = ( - f"Hi {member.first_name},\n\n" - f"{manager_name} invited you to AGMail for {company}.\n\n" - f"Sign in at: {login_url}\n" - f"Work email: {member.email}\n" - f"Temporary password: {initial_password}\n\n" - f"{change_note}\n\n" - "— AGMail" - ) - body_html = ( - f"

Hi {member.first_name},

" - f"

{manager_name} invited you to AGMail for " - f"{company}.

" - f"

Sign in to AGMail

" - f"" - f"

{change_note}

" - f"

— AGMail

" - ) - - sender = f"AGMail " + sender = f"{BRAND_NAME} " await deliver_email( sender=sender, recipients=[member.invitation_email], cc=[], bcc=[], - subject=f"You're invited to {company} on AGMail", + subject=subject, body_text=body_text, body_html=body_html, domain=platform_domain, + reply_to=manager.email, ) diff --git a/backend/tests/test_email_templates.py b/backend/tests/test_email_templates.py new file mode 100644 index 0000000..96cd494 --- /dev/null +++ b/backend/tests/test_email_templates.py @@ -0,0 +1,32 @@ +from app.services.email_templates import build_employee_invitation_email + + +def test_employee_invitation_uses_gentic_branding(): + subject, body_text, body_html = build_employee_invitation_email( + member_first_name="Anurag", + member_email="anurag@road.rentals", + manager_name="Suyash Dixit", + manager_email="suyash@road.rentals", + company_domain="road.rentals", + login_url="https://gentic.email/login", + initial_password="Roadrentals123", + force_password_change=True, + ) + + assert "aGentic Email" in subject + assert subject == ( + "Account invitation: Suyash Dixit added you to road.rentals on aGentic Email" + ) + assert "AGMail" not in subject + assert "Suyash Dixit" in subject + assert "road.rentals" in subject + + assert "Gentic Email" in body_text + assert "AGMail" not in body_text + assert "Suyash Dixit has created a work email account" in body_text + assert "anurag@road.rentals" in body_text + + assert "gentic.email" in body_html + assert "Organization invitation" in body_html + assert "Sign in to aGentic Email" in body_html + assert "AGMail" not in body_html diff --git a/frontend/src/components/AddEmployeeForm.jsx b/frontend/src/components/AddEmployeeForm.jsx index 967c61c..599a585 100644 --- a/frontend/src/components/AddEmployeeForm.jsx +++ b/frontend/src/components/AddEmployeeForm.jsx @@ -242,7 +242,7 @@ export default function AddEmployeeForm({ Invitation email} error={fieldErrors.invitation_email} - hint={`Personal email where AGMail sends the setup invite from @${platformDomain}.`} + hint={`Personal email where aGentic Email sends the setup invite from @${platformDomain}.`} > Require password change on first login - Employee must choose a new password before using AGMail. + Employee must choose a new password before using aGentic Email. Choose a new password

Hi {account.first_name}, your administrator requires you to set a new password - before using AGMail. + before using aGentic Email.