@@ -40,3 +40,156 @@ def send_email(to: str, subject: str, body: str, html: str | None = None) -> Non
4040 subject ,
4141 body ,
4242 )
43+
44+
45+ def send_workspace_invite_email (
46+ to : str ,
47+ workspace_name : str ,
48+ invite_link : str ,
49+ expires_in_hours : int ,
50+ personal_message : str | None = None ,
51+ ) -> None :
52+ """Send a workspace invitation email with an HTML body.
53+
54+ Builds a branded HTML email containing the workspace name, an optional
55+ personal message from the inviter, a prominent call-to-action button with
56+ the acceptance link, and an expiry notice. Falls back to plain-text if HTML
57+ is not supported by the client. Delegates delivery to :func:`send_email`.
58+
59+ Args:
60+ to: Recipient email address.
61+ workspace_name: Name of the workspace the recipient is invited to.
62+ invite_link: Fully-qualified URL the recipient must visit to accept.
63+ expires_in_hours: How many hours until the invite token expires.
64+ personal_message: Optional personal note from the inviting admin.
65+ """
66+ subject = f"You're invited to join workspace '{ workspace_name } '"
67+
68+ # ── Plain-text fallback ───────────────────────────────────────────────
69+ plain_lines = [
70+ "Hello," ,
71+ "" ,
72+ f"You have been invited to join the workspace '{ workspace_name } '." ,
73+ ]
74+ if personal_message :
75+ plain_lines += ["" , personal_message ]
76+ plain_lines += [
77+ "" ,
78+ "Accept your invitation by visiting the link below:" ,
79+ invite_link ,
80+ "" ,
81+ f"This invitation expires in { expires_in_hours } hours." ,
82+ "" ,
83+ "If you did not expect this email, you can safely ignore it." ,
84+ ]
85+ plain_body = "\n " .join (plain_lines )
86+
87+ # ── HTML body ─────────────────────────────────────────────────────────
88+ personal_block = ""
89+ if personal_message :
90+ personal_block = f"""
91+ <tr>
92+ <td style="padding:0 32px 20px;">
93+ <p style="margin:0;padding:16px;background:#f0f4ff;border-left:4px solid #4f46e5;
94+ border-radius:4px;font-size:14px;color:#374151;line-height:1.6;">
95+ { personal_message }
96+ </p>
97+ </td>
98+ </tr>"""
99+
100+ html_body = f"""<!DOCTYPE html>
101+ <html lang="en">
102+ <head>
103+ <meta charset="UTF-8" />
104+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
105+ <title>{ subject } </title>
106+ </head>
107+ <body style="margin:0;padding:0;background:#f3f4f6;font-family:'Segoe UI',Arial,sans-serif;">
108+ <table width="100%" cellpadding="0" cellspacing="0" style="background:#f3f4f6;padding:40px 0;">
109+ <tr>
110+ <td align="center">
111+ <table width="600" cellpadding="0" cellspacing="0"
112+ style="background:#ffffff;border-radius:12px;
113+ box-shadow:0 4px 24px rgba(0,0,0,0.08);overflow:hidden;max-width:600px;">
114+
115+ <!-- Header -->
116+ <tr>
117+ <td style="background:linear-gradient(135deg,#4f46e5 0%,#7c3aed 100%);
118+ padding:36px 32px;text-align:center;">
119+ <h1 style="margin:0;font-size:24px;font-weight:700;color:#ffffff;letter-spacing:-0.5px;">
120+ 📄 PDF Assistant
121+ </h1>
122+ <p style="margin:8px 0 0;font-size:13px;color:#c7d2fe;">
123+ Workspace Invitation
124+ </p>
125+ </td>
126+ </tr>
127+
128+ <!-- Body -->
129+ <tr>
130+ <td style="padding:36px 32px 20px;">
131+ <h2 style="margin:0 0 12px;font-size:20px;font-weight:600;color:#111827;">
132+ You've been invited!
133+ </h2>
134+ <p style="margin:0;font-size:15px;color:#6b7280;line-height:1.6;">
135+ You have been invited to join the workspace
136+ <strong style="color:#111827;">'{ workspace_name } '</strong>
137+ on PDF Assistant. Accept below to start collaborating.
138+ </p>
139+ </td>
140+ </tr>
141+
142+ { personal_block }
143+
144+ <!-- CTA Button -->
145+ <tr>
146+ <td style="padding:8px 32px 32px;text-align:center;">
147+ <a href="{ invite_link } "
148+ style="display:inline-block;padding:14px 36px;
149+ background:linear-gradient(135deg,#4f46e5 0%,#7c3aed 100%);
150+ color:#ffffff;font-size:15px;font-weight:600;
151+ text-decoration:none;border-radius:8px;
152+ box-shadow:0 4px 12px rgba(79,70,229,0.4);">
153+ Accept Invitation →
154+ </a>
155+ <p style="margin:16px 0 0;font-size:12px;color:#9ca3af;">
156+ Or copy this link into your browser:<br/>
157+ <span style="color:#4f46e5;word-break:break-all;">{ invite_link } </span>
158+ </p>
159+ </td>
160+ </tr>
161+
162+ <!-- Expiry notice -->
163+ <tr>
164+ <td style="padding:0 32px 24px;">
165+ <p style="margin:0;padding:12px 16px;background:#fef3c7;border-radius:6px;
166+ font-size:13px;color:#92400e;text-align:center;">
167+ ⏳ This invitation expires in <strong>{ expires_in_hours } hours</strong>.
168+ </p>
169+ </td>
170+ </tr>
171+
172+ <!-- Footer -->
173+ <tr>
174+ <td style="background:#f9fafb;padding:20px 32px;border-top:1px solid #e5e7eb;
175+ text-align:center;">
176+ <p style="margin:0;font-size:12px;color:#9ca3af;line-height:1.6;">
177+ If you did not expect this invitation, you can safely ignore this email.<br/>
178+ This email was sent by PDF Assistant · No reply
179+ </p>
180+ </td>
181+ </tr>
182+
183+ </table>
184+ </td>
185+ </tr>
186+ </table>
187+ </body>
188+ </html>"""
189+
190+ send_email (to , subject , plain_body , html = html_body )
191+ logger .info (
192+ "Workspace invite email dispatched to %s for workspace '%s'" ,
193+ to ,
194+ workspace_name ,
195+ )
0 commit comments