Skip to main content

max / makenotwork

Split email module: extract 25 template methods into email/templates.rs Infrastructure (config, client, suppression, Postmark API) stays in mod.rs (~200 lines). Template methods (send_* composition) moved to templates.rs (~680 lines). No behavior change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-22 05:59 UTC
Commit: cd4064d34674c9552e6e922f267d75164c9bdd51
Parent: c3b55eb
2 files changed, +504 insertions, -488 deletions
@@ -1,8 +1,9 @@
1 - //! Email service for sending transactional emails
1 + //! Email service for sending transactional emails via Postmark.
2 2 //!
3 - //! Currently supports logging emails in development.
4 - //! Can be extended to use Postmark or other providers.
3 + //! - `templates` — email composition methods (one per email type)
4 + //! - `tokens` — HMAC-signed URL generation/verification for email actions
5 5
6 + mod templates;
6 7 mod tokens;
7 8 pub use tokens::*;
8 9
@@ -65,752 +66,8 @@ impl EmailClient {
65 66 }
66 67 }
67 68
68 - /// Send a password reset email
69 - pub async fn send_password_reset(
70 - &self,
71 - to_email: &str,
72 - to_name: Option<&str>,
73 - reset_url: &str,
74 - ) -> Result<()> {
75 - let subject = "Reset your password";
76 - let body = format!(
77 - r#"Hi{name},
78 -
79 - You requested to reset your password. Click the link below to set a new password:
80 -
81 - {url}
82 -
83 - This link expires in 15 minutes. If you didn't request this, you can ignore this email.
84 -
85 - - Makenotwork"#,
86 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
87 - url = reset_url
88 - );
89 -
90 - self.send_email(to_email, subject, &body).await
91 - }
92 -
93 - /// Send an email verification email
94 - pub async fn send_verification(
95 - &self,
96 - to_email: &str,
97 - to_name: Option<&str>,
98 - verify_url: &str,
99 - ) -> Result<()> {
100 - let subject = "Verify your email";
101 - let body = format!(
102 - r#"Hi{name},
103 -
104 - Please verify your email address by clicking the link below:
105 -
106 - {url}
107 -
108 - This link expires in 24 hours.
109 -
110 - - Makenotwork"#,
111 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
112 - url = verify_url
113 - );
114 -
115 - self.send_email(to_email, subject, &body).await
116 - }
117 -
118 - /// Send an account lockout notification
119 - pub async fn send_lockout_notification(
120 - &self,
121 - to_email: &str,
122 - to_name: Option<&str>,
123 - login_link_url: Option<&str>,
124 - ) -> Result<()> {
125 - let subject = "Security alert: Account locked";
126 - let body = format!(
127 - r#"Hi{name},
128 -
129 - Your account has been temporarily locked due to multiple failed login attempts.
130 -
131 - {login_link}
132 -
133 - If this wasn't you, please contact support immediately.
134 -
135 - - Makenotwork"#,
136 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
137 - login_link = login_link_url
138 - .map(|url| format!("Use this link to log in securely:\n\n{}\n\nThis link expires in 15 minutes.", url))
139 - .unwrap_or_else(|| "Your account will unlock automatically in 15 minutes.".to_string())
140 - );
141 -
142 - self.send_email(to_email, subject, &body).await
143 - }
144 -
145 - /// Send a one-time login link
146 - pub async fn send_login_link(
147 - &self,
148 - to_email: &str,
149 - to_name: Option<&str>,
150 - login_url: &str,
151 - ) -> Result<()> {
152 - let subject = "Your login link";
153 - let body = format!(
154 - r#"Hi{name},
155 -
156 - Click the link below to log in to your account:
157 -
158 - {url}
159 -
160 - This link expires in 15 minutes and can only be used once.
161 -
162 - If you didn't request this, you can ignore this email.
163 -
164 - - Makenotwork"#,
165 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
166 - url = login_url
167 - );
168 -
169 - self.send_email(to_email, subject, &body).await
170 - }
171 -
172 - /// Send account deletion confirmation email
173 - pub async fn send_deletion_confirmation(
174 - &self,
175 - to_email: &str,
176 - to_name: Option<&str>,
177 - delete_url: &str,
178 - ) -> Result<()> {
179 - let subject = "Confirm account deletion";
180 - let body = format!(
181 - r#"Hi{name},
182 -
183 - You requested to delete your Makenotwork account.
184 -
185 - IMPORTANT: This action is permanent and cannot be undone.
186 -
187 - Clicking the link below will immediately and permanently delete:
188 - - All your projects and items
189 - - All uploaded content (audio, images)
190 - - Your profile and account settings
191 - - Your custom links
192 -
193 - Purchases made by your fans will remain accessible to them.
194 -
195 - To confirm deletion, click this link:
196 -
197 - {url}
198 -
199 - This link expires in 1 hour.
200 -
201 - If you did not request this, do NOT click the link. Your account is safe.
202 -
203 - - Makenotwork"#,
204 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
205 - url = delete_url
206 - );
207 -
208 - self.send_email(to_email, subject, &body).await
209 - }
210 -
211 - /// Send a purchase confirmation email
212 - pub async fn send_purchase_confirmation(
213 - &self,
214 - to_email: &str,
215 - to_name: Option<&str>,
216 - item_title: &str,
217 - price: &str,
218 - ) -> Result<()> {
219 - let subject = "Your purchase is confirmed";
220 - let body = format!(
221 - r#"Hi{name},
222 -
223 - Your purchase of {item} ({price}) is confirmed.
224 -
225 - You can access your purchase from your library at any time.
226 -
227 - - Makenotwork"#,
228 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
229 - item = item_title,
230 - price = price
231 - );
232 -
233 - self.send_email(to_email, subject, &body).await
234 - }
235 -
236 - /// Send a subscription started email
237 - pub async fn send_subscription_started(
238 - &self,
239 - to_email: &str,
240 - to_name: Option<&str>,
241 - tier_name: &str,
242 - project_title: &str,
243 - price: &str,
244 - ) -> Result<()> {
245 - let subject = &format!("You're subscribed to {}", project_title);
246 - let body = format!(
247 - r#"Hi{name},
248 -
249 - You're now subscribed to {project} ({tier} - {price}/mo).
250 -
251 - You have access to all content included in this tier. Your subscription will renew automatically each month.
252 -
253 - - Makenotwork"#,
254 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
255 - project = project_title,
256 - tier = tier_name,
257 - price = price
258 - );
259 -
260 - self.send_email(to_email, subject, &body).await
261 - }
262 -
263 - /// Send a subscription cancelled email
264 - pub async fn send_subscription_cancelled(
265 - &self,
266 - to_email: &str,
267 - to_name: Option<&str>,
268 - tier_name: &str,
269 - project_title: &str,
270 - ) -> Result<()> {
271 - let subject = "Your subscription has been cancelled";
272 - let body = format!(
273 - r#"Hi{name},
274 -
275 - Your subscription to {project} ({tier}) has been cancelled.
276 -
277 - You will retain access until the end of your current billing period.
278 -
279 - - Makenotwork"#,
280 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
281 - project = project_title,
282 - tier = tier_name
283 - );
284 -
285 - self.send_email(to_email, subject, &body).await
286 - }
287 -
288 - /// Send a subscription renewed email
289 - pub async fn send_subscription_renewed(
290 - &self,
291 - to_email: &str,
292 - to_name: Option<&str>,
293 - tier_name: &str,
294 - price: &str,
295 - ) -> Result<()> {
296 - let subject = "Your subscription has been renewed";
297 - let body = format!(
298 - r#"Hi{name},
299 -
300 - Your subscription ({tier} - {price}/mo) has been renewed for another month.
301 -
302 - - Makenotwork"#,
303 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
304 - tier = tier_name,
305 - price = price
306 - );
307 -
308 - self.send_email(to_email, subject, &body).await
309 - }
310 -
311 - /// Send a new-device login notification
312 - pub async fn send_new_login_notification(
313 - &self,
314 - to_email: &str,
315 - to_name: Option<&str>,
316 - device: Option<&str>,
317 - ip: Option<&str>,
318 - unsub_url: Option<&str>,
319 - ) -> Result<()> {
320 - let subject = "New sign-in to your account";
321 - let device_line = device.unwrap_or("Unknown device");
322 - let ip_line = ip.unwrap_or("Unknown");
323 - let body = format!(
324 - r#"Hi{name},
325 -
326 - Your account was just signed in to from a new device.
327 -
328 - Device: {device}
329 - IP address: {ip}
330 -
331 - If this was you, no action is needed. If you don't recognize this sign-in, go to your dashboard and revoke the session under Settings > Sessions.
332 -
333 - - Makenotwork"#,
334 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
335 - device = device_line,
336 - ip = ip_line,
337 - );
338 -
339 - self.send_email_with_unsub(to_email, subject, &body, unsub_url).await
340 - }
341 -
342 - /// Send a suspension notification to a user
343 - pub async fn send_suspension_notification(
344 - &self,
345 - to_email: &str,
346 - to_name: Option<&str>,
347 - reason: &str,
348 - ) -> Result<()> {
349 - let subject = "Your account has been suspended";
350 - let body = format!(
351 - r#"Hi{name},
352 -
353 - Your Makenotwork account has been suspended.
354 -
355 - Reason: {reason}
356 -
357 - You can appeal this decision from your dashboard. You can also export your data at any time.
358 -
359 - Log in to your dashboard to submit an appeal or export your data.
360 -
361 - - Makenotwork"#,
362 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
363 - reason = reason
364 - );
365 -
366 - self.send_email(to_email, subject, &body).await
367 - }
368 -
369 - /// Send an appeal decision notification to a user
370 - pub async fn send_appeal_decision(
371 - &self,
372 - to_email: &str,
373 - to_name: Option<&str>,
374 - decision: &str,
375 - response: &str,
376 - ) -> Result<()> {
377 - let outcome = if decision == "approved" {
378 - "Your account has been reinstated"
379 - } else {
380 - "Your appeal has been denied"
381 - };
382 -
383 - let subject = "Your appeal has been reviewed";
384 - let body = format!(
385 - r#"Hi{name},
386 -
387 - {outcome}.
388 -
389 - Response from the review team:
390 -
391 - {response}
392 -
393 - Log in to your dashboard for more details.
394 -
395 - - Makenotwork"#,
396 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
397 - outcome = outcome,
398 - response = response
399 - );
400 -
401 - self.send_email(to_email, subject, &body).await
402 - }
403 -
404 - /// Send a platform shutdown notice to a user
405 - pub async fn send_shutdown_notice(
406 - &self,
407 - to_email: &str,
408 - to_name: Option<&str>,
409 - shutdown_date: &str,
410 - ) -> Result<()> {
411 - let subject = "Important: Makenot.work is shutting down";
412 - let body = format!(
413 - r#"Hi{name},
414 -
415 - We are writing to let you know that Makenot.work will be shutting down on {shutdown_date}.
416 -
417 - You have at least 90 days from today to export all of your data. Your projects, content, sales history, and follower data can all be exported from your dashboard.
418 -
419 - To export your data, log in and visit your dashboard export page.
420 -
421 - We built Makenotwork on the principle of no lock-in, and we intend to honor that through the end. Thank you for being part of this.
422 -
423 - - Makenotwork"#,
424 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
425 - shutdown_date = shutdown_date
426 - );
427 -
428 - self.send_email(to_email, subject, &body).await
429 - }
430 -
431 - /// Notify a creator that their invite code was redeemed by a new user.
432 - pub async fn send_invite_redeemed(
433 - &self,
434 - to_email: &str,
435 - to_name: Option<&str>,
436 - invitee_username: &str,
437 - ) -> Result<()> {
438 - let subject = "Your invite was used";
439 - let body = format!(
440 - r#"Hi{name},
441 -
442 - {invitee} just signed up using one of your invite codes. Their account is pending admin approval.
443 -
444 - - Makenotwork"#,
445 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
446 - invitee = invitee_username
447 - );
448 -
449 - self.send_email(to_email, subject, &body).await
450 - }
451 -
452 - /// Notify a creator that someone bought their content.
453 - pub async fn send_sale_notification(
454 - &self,
455 - to_email: &str,
456 - to_name: Option<&str>,
457 - buyer_username: &str,
458 - item_title: &str,
459 - price: &str,
460 - unsub_url: Option<&str>,
461 - ) -> Result<()> {
462 - let subject = format!("New sale: {}", item_title);
463 - let body = format!(
464 - r#"Hi{name},
465 -
466 - {buyer} just purchased {item} for {price}.
467 -
468 - View your sales from your dashboard.
469 -
470 - - Makenotwork"#,
471 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
472 - buyer = buyer_username,
473 - item = item_title,
474 - price = price,
475 - );
476 -
477 - self.send_email_with_unsub(to_email, &subject, &body, unsub_url).await
478 - }
479 -
480 - /// Notify a creator that someone followed them or their project.
481 - pub async fn send_follower_notification(
482 - &self,
483 - to_email: &str,
484 - to_name: Option<&str>,
485 - follower_username: &str,
486 - context: &str,
487 - unsub_url: Option<&str>,
488 - ) -> Result<()> {
489 - let subject = "New follower";
490 - let body = format!(
491 - r#"Hi{name},
492 -
493 - {follower} is now following {context}.
494 -
495 - - Makenotwork"#,
496 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
497 - follower = follower_username,
498 - context = context,
499 - );
500 -
501 - self.send_email_with_unsub(to_email, subject, &body, unsub_url).await
502 - }
503 -
504 - /// Send a creator's broadcast message to a follower.
505 - pub async fn send_broadcast(
506 - &self,
507 - to_email: &str,
508 - to_name: Option<&str>,
509 - creator_name: &str,
510 - subject: &str,
511 - body_text: &str,
512 - unsub_url: Option<&str>,
513 - ) -> Result<()> {
514 - let subject = format!("{} -- from {}", subject, creator_name);
515 - let body = format!(
516 - r#"Hi{name},
517 -
518 - {body}
519 -
520 - --
521 - You received this because you follow {creator} on Makenotwork.
522 -
523 - - Makenotwork"#,
524 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
525 - body = body_text,
526 - creator = creator_name,
527 - );
528 -
529 - self.send_email_broadcast_with_unsub(to_email, &subject, &body, unsub_url).await
530 - }
531 -
532 - /// Notify followers about a new release.
533 - pub async fn send_release_announcement(
534 - &self,
535 - to_email: &str,
536 - to_name: Option<&str>,
537 - creator_name: &str,
538 - item_title: &str,
539 - item_url: &str,
540 - unsub_url: Option<&str>,
541 - ) -> Result<()> {
542 - let subject = format!("New release: {} by {}", item_title, creator_name);
543 - let body = format!(
544 - r#"Hi{name},
545 -
546 - {creator} just published something new: {item}
547 -
548 - Check it out: {url}
549 -
550 - - Makenotwork"#,
551 - name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
552 - creator = creator_name,
Lines truncated
@@ -0,0 +1,753 @@
1 + //! Email template methods for composing and sending transactional emails.
2 + //!
3 + //! Each method here formats a specific email type and delegates to the
4 + //! sending infrastructure in `super::EmailClient`.
5 +
6 + use crate::error::Result;
7 + use super::EmailClient;
8 +
9 + impl EmailClient {
10 + /// Send a password reset email
11 + pub async fn send_password_reset(
12 + &self,
13 + to_email: &str,
14 + to_name: Option<&str>,
15 + reset_url: &str,
16 + ) -> Result<()> {
17 + let subject = "Reset your password";
18 + let body = format!(
19 + r#"Hi{name},
20 +
21 + You requested to reset your password. Click the link below to set a new password:
22 +
23 + {url}
24 +
25 + This link expires in 15 minutes. If you didn't request this, you can ignore this email.
26 +
27 + - Makenotwork"#,
28 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
29 + url = reset_url
30 + );
31 +
32 + self.send_email(to_email, subject, &body).await
33 + }
34 +
35 + /// Send an email verification email
36 + pub async fn send_verification(
37 + &self,
38 + to_email: &str,
39 + to_name: Option<&str>,
40 + verify_url: &str,
41 + ) -> Result<()> {
42 + let subject = "Verify your email";
43 + let body = format!(
44 + r#"Hi{name},
45 +
46 + Please verify your email address by clicking the link below:
47 +
48 + {url}
49 +
50 + This link expires in 24 hours.
51 +
52 + - Makenotwork"#,
53 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
54 + url = verify_url
55 + );
56 +
57 + self.send_email(to_email, subject, &body).await
58 + }
59 +
60 + /// Send an account lockout notification
61 + pub async fn send_lockout_notification(
62 + &self,
63 + to_email: &str,
64 + to_name: Option<&str>,
65 + login_link_url: Option<&str>,
66 + ) -> Result<()> {
67 + let subject = "Security alert: Account locked";
68 + let body = format!(
69 + r#"Hi{name},
70 +
71 + Your account has been temporarily locked due to multiple failed login attempts.
72 +
73 + {login_link}
74 +
75 + If this wasn't you, please contact support immediately.
76 +
77 + - Makenotwork"#,
78 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
79 + login_link = login_link_url
80 + .map(|url| format!("Use this link to log in securely:\n\n{}\n\nThis link expires in 15 minutes.", url))
81 + .unwrap_or_else(|| "Your account will unlock automatically in 15 minutes.".to_string())
82 + );
83 +
84 + self.send_email(to_email, subject, &body).await
85 + }
86 +
87 + /// Send a one-time login link
88 + pub async fn send_login_link(
89 + &self,
90 + to_email: &str,
91 + to_name: Option<&str>,
92 + login_url: &str,
93 + ) -> Result<()> {
94 + let subject = "Your login link";
95 + let body = format!(
96 + r#"Hi{name},
97 +
98 + Click the link below to log in to your account:
99 +
100 + {url}
101 +
102 + This link expires in 15 minutes and can only be used once.
103 +
104 + If you didn't request this, you can ignore this email.
105 +
106 + - Makenotwork"#,
107 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
108 + url = login_url
109 + );
110 +
111 + self.send_email(to_email, subject, &body).await
112 + }
113 +
114 + /// Send account deletion confirmation email
115 + pub async fn send_deletion_confirmation(
116 + &self,
117 + to_email: &str,
118 + to_name: Option<&str>,
119 + delete_url: &str,
120 + ) -> Result<()> {
121 + let subject = "Confirm account deletion";
122 + let body = format!(
123 + r#"Hi{name},
124 +
125 + You requested to delete your Makenotwork account.
126 +
127 + IMPORTANT: This action is permanent and cannot be undone.
128 +
129 + Clicking the link below will immediately and permanently delete:
130 + - All your projects and items
131 + - All uploaded content (audio, images)
132 + - Your profile and account settings
133 + - Your custom links
134 +
135 + Purchases made by your fans will remain accessible to them.
136 +
137 + To confirm deletion, click this link:
138 +
139 + {url}
140 +
141 + This link expires in 1 hour.
142 +
143 + If you did not request this, do NOT click the link. Your account is safe.
144 +
145 + - Makenotwork"#,
146 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
147 + url = delete_url
148 + );
149 +
150 + self.send_email(to_email, subject, &body).await
151 + }
152 +
153 + /// Send a purchase confirmation email
154 + pub async fn send_purchase_confirmation(
155 + &self,
156 + to_email: &str,
157 + to_name: Option<&str>,
158 + item_title: &str,
159 + price: &str,
160 + ) -> Result<()> {
161 + let subject = "Your purchase is confirmed";
162 + let body = format!(
163 + r#"Hi{name},
164 +
165 + Your purchase of {item} ({price}) is confirmed.
166 +
167 + You can access your purchase from your library at any time.
168 +
169 + - Makenotwork"#,
170 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
171 + item = item_title,
172 + price = price
173 + );
174 +
175 + self.send_email(to_email, subject, &body).await
176 + }
177 +
178 + /// Send a subscription started email
179 + pub async fn send_subscription_started(
180 + &self,
181 + to_email: &str,
182 + to_name: Option<&str>,
183 + tier_name: &str,
184 + project_title: &str,
185 + price: &str,
186 + ) -> Result<()> {
187 + let subject = &format!("You're subscribed to {}", project_title);
188 + let body = format!(
189 + r#"Hi{name},
190 +
191 + You're now subscribed to {project} ({tier} - {price}/mo).
192 +
193 + You have access to all content included in this tier. Your subscription will renew automatically each month.
194 +
195 + - Makenotwork"#,
196 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
197 + project = project_title,
198 + tier = tier_name,
199 + price = price
200 + );
201 +
202 + self.send_email(to_email, subject, &body).await
203 + }
204 +
205 + /// Send a subscription cancelled email
206 + pub async fn send_subscription_cancelled(
207 + &self,
208 + to_email: &str,
209 + to_name: Option<&str>,
210 + tier_name: &str,
211 + project_title: &str,
212 + ) -> Result<()> {
213 + let subject = "Your subscription has been cancelled";
214 + let body = format!(
215 + r#"Hi{name},
216 +
217 + Your subscription to {project} ({tier}) has been cancelled.
218 +
219 + You will retain access until the end of your current billing period.
220 +
221 + - Makenotwork"#,
222 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
223 + project = project_title,
224 + tier = tier_name
225 + );
226 +
227 + self.send_email(to_email, subject, &body).await
228 + }
229 +
230 + /// Send a subscription renewed email
231 + pub async fn send_subscription_renewed(
232 + &self,
233 + to_email: &str,
234 + to_name: Option<&str>,
235 + tier_name: &str,
236 + price: &str,
237 + ) -> Result<()> {
238 + let subject = "Your subscription has been renewed";
239 + let body = format!(
240 + r#"Hi{name},
241 +
242 + Your subscription ({tier} - {price}/mo) has been renewed for another month.
243 +
244 + - Makenotwork"#,
245 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
246 + tier = tier_name,
247 + price = price
248 + );
249 +
250 + self.send_email(to_email, subject, &body).await
251 + }
252 +
253 + /// Send a new-device login notification
254 + pub async fn send_new_login_notification(
255 + &self,
256 + to_email: &str,
257 + to_name: Option<&str>,
258 + device: Option<&str>,
259 + ip: Option<&str>,
260 + unsub_url: Option<&str>,
261 + ) -> Result<()> {
262 + let subject = "New sign-in to your account";
263 + let device_line = device.unwrap_or("Unknown device");
264 + let ip_line = ip.unwrap_or("Unknown");
265 + let body = format!(
266 + r#"Hi{name},
267 +
268 + Your account was just signed in to from a new device.
269 +
270 + Device: {device}
271 + IP address: {ip}
272 +
273 + If this was you, no action is needed. If you don't recognize this sign-in, go to your dashboard and revoke the session under Settings > Sessions.
274 +
275 + - Makenotwork"#,
276 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
277 + device = device_line,
278 + ip = ip_line,
279 + );
280 +
281 + self.send_email_with_unsub(to_email, subject, &body, unsub_url).await
282 + }
283 +
284 + /// Send a suspension notification to a user
285 + pub async fn send_suspension_notification(
286 + &self,
287 + to_email: &str,
288 + to_name: Option<&str>,
289 + reason: &str,
290 + ) -> Result<()> {
291 + let subject = "Your account has been suspended";
292 + let body = format!(
293 + r#"Hi{name},
294 +
295 + Your Makenotwork account has been suspended.
296 +
297 + Reason: {reason}
298 +
299 + You can appeal this decision from your dashboard. You can also export your data at any time.
300 +
301 + Log in to your dashboard to submit an appeal or export your data.
302 +
303 + - Makenotwork"#,
304 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
305 + reason = reason
306 + );
307 +
308 + self.send_email(to_email, subject, &body).await
309 + }
310 +
311 + /// Send an appeal decision notification to a user
312 + pub async fn send_appeal_decision(
313 + &self,
314 + to_email: &str,
315 + to_name: Option<&str>,
316 + decision: &str,
317 + response: &str,
318 + ) -> Result<()> {
319 + let outcome = if decision == "approved" {
320 + "Your account has been reinstated"
321 + } else {
322 + "Your appeal has been denied"
323 + };
324 +
325 + let subject = "Your appeal has been reviewed";
326 + let body = format!(
327 + r#"Hi{name},
328 +
329 + {outcome}.
330 +
331 + Response from the review team:
332 +
333 + {response}
334 +
335 + Log in to your dashboard for more details.
336 +
337 + - Makenotwork"#,
338 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
339 + outcome = outcome,
340 + response = response
341 + );
342 +
343 + self.send_email(to_email, subject, &body).await
344 + }
345 +
346 + /// Send a platform shutdown notice to a user
347 + pub async fn send_shutdown_notice(
348 + &self,
349 + to_email: &str,
350 + to_name: Option<&str>,
351 + shutdown_date: &str,
352 + ) -> Result<()> {
353 + let subject = "Important: Makenot.work is shutting down";
354 + let body = format!(
355 + r#"Hi{name},
356 +
357 + We are writing to let you know that Makenot.work will be shutting down on {shutdown_date}.
358 +
359 + You have at least 90 days from today to export all of your data. Your projects, content, sales history, and follower data can all be exported from your dashboard.
360 +
361 + To export your data, log in and visit your dashboard export page.
362 +
363 + We built Makenotwork on the principle of no lock-in, and we intend to honor that through the end. Thank you for being part of this.
364 +
365 + - Makenotwork"#,
366 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
367 + shutdown_date = shutdown_date
368 + );
369 +
370 + self.send_email(to_email, subject, &body).await
371 + }
372 +
373 + /// Notify a creator that their invite code was redeemed by a new user.
374 + pub async fn send_invite_redeemed(
375 + &self,
376 + to_email: &str,
377 + to_name: Option<&str>,
378 + invitee_username: &str,
379 + ) -> Result<()> {
380 + let subject = "Your invite was used";
381 + let body = format!(
382 + r#"Hi{name},
383 +
384 + {invitee} just signed up using one of your invite codes. Their account is pending admin approval.
385 +
386 + - Makenotwork"#,
387 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
388 + invitee = invitee_username
389 + );
390 +
391 + self.send_email(to_email, subject, &body).await
392 + }
393 +
394 + /// Notify a creator that someone bought their content.
395 + pub async fn send_sale_notification(
396 + &self,
397 + to_email: &str,
398 + to_name: Option<&str>,
399 + buyer_username: &str,
400 + item_title: &str,
401 + price: &str,
402 + unsub_url: Option<&str>,
403 + ) -> Result<()> {
404 + let subject = format!("New sale: {}", item_title);
405 + let body = format!(
406 + r#"Hi{name},
407 +
408 + {buyer} just purchased {item} for {price}.
409 +
410 + View your sales from your dashboard.
411 +
412 + - Makenotwork"#,
413 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
414 + buyer = buyer_username,
415 + item = item_title,
416 + price = price,
417 + );
418 +
419 + self.send_email_with_unsub(to_email, &subject, &body, unsub_url).await
420 + }
421 +
422 + /// Notify a creator that someone followed them or their project.
423 + pub async fn send_follower_notification(
424 + &self,
425 + to_email: &str,
426 + to_name: Option<&str>,
427 + follower_username: &str,
428 + context: &str,
429 + unsub_url: Option<&str>,
430 + ) -> Result<()> {
431 + let subject = "New follower";
432 + let body = format!(
433 + r#"Hi{name},
434 +
435 + {follower} is now following {context}.
436 +
437 + - Makenotwork"#,
438 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
439 + follower = follower_username,
440 + context = context,
441 + );
442 +
443 + self.send_email_with_unsub(to_email, subject, &body, unsub_url).await
444 + }
445 +
446 + /// Send a creator's broadcast message to a follower.
447 + pub async fn send_broadcast(
448 + &self,
449 + to_email: &str,
450 + to_name: Option<&str>,
451 + creator_name: &str,
452 + subject: &str,
453 + body_text: &str,
454 + unsub_url: Option<&str>,
455 + ) -> Result<()> {
456 + let subject = format!("{} -- from {}", subject, creator_name);
457 + let body = format!(
458 + r#"Hi{name},
459 +
460 + {body}
461 +
462 + --
463 + You received this because you follow {creator} on Makenotwork.
464 +
465 + - Makenotwork"#,
466 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
467 + body = body_text,
468 + creator = creator_name,
469 + );
470 +
471 + self.send_email_broadcast_with_unsub(to_email, &subject, &body, unsub_url).await
472 + }
473 +
474 + /// Notify followers about a new release.
475 + pub async fn send_release_announcement(
476 + &self,
477 + to_email: &str,
478 + to_name: Option<&str>,
479 + creator_name: &str,
480 + item_title: &str,
481 + item_url: &str,
482 + unsub_url: Option<&str>,
483 + ) -> Result<()> {
484 + let subject = format!("New release: {} by {}", item_title, creator_name);
485 + let body = format!(
486 + r#"Hi{name},
487 +
488 + {creator} just published something new: {item}
489 +
490 + Check it out: {url}
491 +
492 + - Makenotwork"#,
493 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
494 + creator = creator_name,
495 + item = item_title,
496 + url = item_url,
497 + );
498 +
499 + self.send_email_broadcast_with_unsub(to_email, &subject, &body, unsub_url).await
500 + }
Lines truncated