77from datetime import UTC , date , datetime
88from typing import Self
99
10+ from couchers import urls
1011from couchers .email .rendering import (
1112 EmailBlock ,
1213 EmailBlocksBuilder ,
1314 ParaBlock ,
15+ UserInfo ,
1416 get_emails_i18next ,
1517)
1618from couchers .i18n import LocalizationContext
@@ -76,6 +78,77 @@ def _body_builder(self, loc_context: LocalizationContext) -> EmailBlocksBuilder:
7678# Specific email definitions
7779
7880
81+ @dataclass (kw_only = True , slots = True )
82+ class AccountDeletionStartedEmail (EmailBase ):
83+ """Sent to a user to confirm their account deletion request."""
84+
85+ deletion_link : str
86+
87+ @property
88+ def string_key_prefix (self ) -> str :
89+ return "account_deletion_started"
90+
91+ def build_body (self , builder : EmailBlocksBuilder , loc_context : LocalizationContext ) -> None :
92+ builder .para ("request_description" )
93+ builder .para ("confirmation_instructions" )
94+ builder .action (self .deletion_link , "confirm_action" )
95+ builder .security_warning_para ()
96+
97+ @classmethod
98+ def dummy_data (cls ) -> AccountDeletionStartedEmail :
99+ return AccountDeletionStartedEmail (
100+ user_name = "Alice" ,
101+ deletion_link = "https://couchers.org/delete-account?token=xxx" ,
102+ )
103+
104+
105+ @dataclass (kw_only = True , slots = True )
106+ class AccountDeletionCompletedEmail (EmailBase ):
107+ """Sent to a user after their account has been deleted."""
108+
109+ undelete_link : str
110+ days : int
111+
112+ @property
113+ def string_key_prefix (self ) -> str :
114+ return "account_deletion_completed"
115+
116+ def build_body (self , builder : EmailBlocksBuilder , loc_context : LocalizationContext ) -> None :
117+ builder .para ("confirmation" )
118+ builder .para ("farewell" )
119+ builder .para ("recovery_instructions_days" , {"count" : self .days })
120+ builder .action (self .undelete_link , "recover_action" )
121+ builder .security_warning_para ()
122+
123+ @classmethod
124+ def dummy_data (cls ) -> AccountDeletionCompletedEmail :
125+ return AccountDeletionCompletedEmail (
126+ user_name = "Alice" ,
127+ undelete_link = "https://couchers.org/recover-account?token=xxx" ,
128+ days = 30 ,
129+ )
130+
131+
132+ @dataclass (kw_only = True , slots = True )
133+ class AccountDeletionRecoveredEmail (EmailBase ):
134+ """Sent to a user after their account deletion has been cancelled."""
135+
136+ @property
137+ def string_key_prefix (self ) -> str :
138+ return "account_deletion_recovered"
139+
140+ def build_body (self , builder : EmailBlocksBuilder , loc_context : LocalizationContext ) -> None :
141+ builder .para ("confirmation" )
142+ builder .para ("login_instructions" )
143+ builder .action (urls .app_link (), "login_action" )
144+ builder .para ("redelete_instructions" )
145+ builder .security_warning_para ()
146+
147+ @classmethod
148+ def dummy_data (cls ) -> AccountDeletionRecoveredEmail :
149+ return AccountDeletionRecoveredEmail (user_name = "Alice" )
150+
151+
79152@dataclass (kw_only = True , slots = True )
80153class APIKeyIssuedEmail (EmailBase ):
81154 """Sent to a user to notify them that their API key was issued."""
@@ -89,7 +162,7 @@ def string_key_prefix(self) -> str:
89162
90163 def build_body (self , builder : EmailBlocksBuilder , loc_context : LocalizationContext ) -> None :
91164 builder .para ("header" )
92- builder .quote (self .api_key )
165+ builder .quote (self .api_key , markdown = False )
93166 builder .para ("expiry" , {"datetime" : loc_context .localize_datetime (self .expiry )})
94167 builder .para ("usage_warning" )
95168 builder .para ("policy_warning" )
@@ -146,6 +219,92 @@ def dummy_data(cls) -> BirthdateChangedEmail:
146219 )
147220
148221
222+ @dataclass (kw_only = True , slots = True )
223+ class DiscussionCreatedEmail (EmailBase ):
224+ """Sent to a user when a new discussion is created in a community they follow."""
225+
226+ author : UserInfo
227+ title : str
228+ parent_context : str # Community or group name
229+ markdown_text : str
230+ view_link : str
231+
232+ @property
233+ def string_key_prefix (self ) -> str :
234+ return "discussion_created"
235+
236+ def get_subject_line (self , loc_context : LocalizationContext ) -> str :
237+ return self ._localize (loc_context , "subject" , {"author" : self .author .name , "title" : self .title })
238+
239+ def build_body (self , builder : EmailBlocksBuilder , loc_context : LocalizationContext ) -> None :
240+ builder .para (
241+ "body" ,
242+ {
243+ "author" : self .author .name ,
244+ "title" : self .title ,
245+ "parent_context" : self .parent_context ,
246+ },
247+ )
248+ builder .user (self .author )
249+ builder .quote (self .markdown_text , markdown = True )
250+ builder .action (self .view_link , "view_action" )
251+
252+ @classmethod
253+ def dummy_data (cls ) -> DiscussionCreatedEmail :
254+ return DiscussionCreatedEmail (
255+ user_name = "Alice" ,
256+ author = UserInfo .dummy_bob (),
257+ title = "Best hiking trails near Berlin" ,
258+ parent_context = "Berlin Community" ,
259+ markdown_text = "I've been exploring the area and found some **great** spots..." ,
260+ view_link = "https://couchers.org/discussions/123" ,
261+ )
262+
263+
264+ @dataclass (kw_only = True , slots = True )
265+ class DiscussionCommentEmail (EmailBase ):
266+ """Sent to a user when someone comments on a discussion they follow."""
267+
268+ author : UserInfo
269+ discussion_title : str
270+ discussion_parent_context : str # Community or group name
271+ markdown_text : str
272+ view_link : str
273+
274+ @property
275+ def string_key_prefix (self ) -> str :
276+ return "discussion_comment"
277+
278+ def get_subject_line (self , loc_context : LocalizationContext ) -> str :
279+ return self ._localize (
280+ loc_context , "subject" , {"author" : self .author .name , "discussion_title" : self .discussion_title }
281+ )
282+
283+ def build_body (self , builder : EmailBlocksBuilder , loc_context : LocalizationContext ) -> None :
284+ builder .para (
285+ "body" ,
286+ {
287+ "author" : self .author .name ,
288+ "discussion_title" : self .discussion_title ,
289+ "parent_context" : self .discussion_parent_context ,
290+ },
291+ )
292+ builder .user (self .author )
293+ builder .quote (self .markdown_text , markdown = True )
294+ builder .action (self .view_link , "view_action" )
295+
296+ @classmethod
297+ def dummy_data (cls ) -> DiscussionCommentEmail :
298+ return DiscussionCommentEmail (
299+ user_name = "Alice" ,
300+ author = UserInfo .dummy_bob (),
301+ discussion_title = "Best hiking trails near Berlin" ,
302+ discussion_parent_context = "Berlin Community" ,
303+ markdown_text = "Great recommendations, I also **love** the Grünewald forest!" ,
304+ view_link = "https://couchers.org/discussions/123" ,
305+ )
306+
307+
149308@dataclass (kw_only = True , slots = True )
150309class EmailAddressChangedEmail (EmailBase ):
151310 """Sent to a user to notify them that their email address was changed."""
@@ -182,6 +341,67 @@ def dummy_data(cls) -> EmailAddressVerifiedEmail:
182341 return EmailAddressVerifiedEmail (user_name = "Alice" )
183342
184343
344+ @dataclass (kw_only = True , slots = True )
345+ class FriendRequestReceivedEmail (EmailBase ):
346+ """Sent to a user when they receive a friend request."""
347+
348+ befriender : UserInfo
349+
350+ @property
351+ def string_key_prefix (self ) -> str :
352+ return "friend_request_received"
353+
354+ def get_subject_line (self , loc_context : LocalizationContext ) -> str :
355+ return self ._localize (loc_context , "subject" , {"name" : self .befriender .name })
356+
357+ def get_preview_line (self , loc_context : LocalizationContext ) -> str :
358+ return self ._localize (loc_context , "body" , {"name" : self .befriender .name })
359+
360+ def build_body (self , builder : EmailBlocksBuilder , loc_context : LocalizationContext ) -> None :
361+ builder .para ("body" , {"name" : self .befriender .name })
362+ builder .user (self .befriender )
363+ builder .action (urls .friend_requests_link (), "view_action" )
364+ builder .para ("closing" )
365+ builder .do_not_reply_request_para ()
366+
367+ @classmethod
368+ def dummy_data (cls ) -> FriendRequestReceivedEmail :
369+ return FriendRequestReceivedEmail (
370+ user_name = "Alice" ,
371+ befriender = UserInfo .dummy_bob (),
372+ )
373+
374+
375+ @dataclass (kw_only = True , slots = True )
376+ class FriendRequestAcceptedEmail (EmailBase ):
377+ """Sent to a user when their friend request is accepted."""
378+
379+ new_friend : UserInfo
380+
381+ @property
382+ def string_key_prefix (self ) -> str :
383+ return "friend_request_accepted"
384+
385+ def get_subject_line (self , loc_context : LocalizationContext ) -> str :
386+ return self ._localize (loc_context , "subject" , {"name" : self .new_friend .name })
387+
388+ def get_preview_line (self , loc_context : LocalizationContext ) -> str :
389+ return self ._localize (loc_context , "body" , {"name" : self .new_friend .name })
390+
391+ def build_body (self , builder : EmailBlocksBuilder , loc_context : LocalizationContext ) -> None :
392+ builder .para ("body" , {"name" : self .new_friend .name })
393+ builder .user (self .new_friend )
394+ builder .action (self .new_friend .profile_url , "view_action" )
395+ builder .para ("closing" )
396+
397+ @classmethod
398+ def dummy_data (cls ) -> FriendRequestAcceptedEmail :
399+ return FriendRequestAcceptedEmail (
400+ user_name = "Alice" ,
401+ new_friend = UserInfo .dummy_bob (),
402+ )
403+
404+
185405@dataclass (kw_only = True , slots = True )
186406class GenderChangedEmail (EmailBase ):
187407 """Sent to a user to notify them that their gender was changed."""
@@ -400,8 +620,6 @@ def dummy_data(cls) -> StrongVerificationFailedEmail:
400620class StrongVerificationSucceededEmail (EmailBase ):
401621 """Sent to a user when their strong verification has succeeded."""
402622
403- donate_link : str
404-
405623 @property
406624 def string_key_prefix (self ) -> str :
407625 return "strong_verification_succeeded"
@@ -411,11 +629,45 @@ def build_body(self, builder: EmailBlocksBuilder, loc_context: LocalizationConte
411629 builder .para ("thanks_message" )
412630 builder .para ("cost_explanation" )
413631 builder .para ("donation_request" )
414- builder .action (self .donate_link , "donate_action" )
632+ donate_link = urls .donation_url () + "?utm_source=strong-verification-email"
633+ builder .action (donate_link , "donate_action" )
415634 builder .security_warning_para ()
416635
417636 @classmethod
418637 def dummy_data (cls ) -> StrongVerificationSucceededEmail :
419- return StrongVerificationSucceededEmail (
420- user_name = "Alice" , donate_link = "https://couchers.org/donate?utm_source=strong-verification-email"
638+ return StrongVerificationSucceededEmail (user_name = "Alice" )
639+
640+
641+ @dataclass (kw_only = True , slots = True )
642+ class ThreadReplyEmail (EmailBase ):
643+ """Sent to a user when someone replies in a comment thread they participated in."""
644+
645+ author : UserInfo
646+ parent_context : str # Title of the event or discussion being replied in
647+ markdown_text : str
648+ view_link : str
649+
650+ @property
651+ def string_key_prefix (self ) -> str :
652+ return "thread_reply"
653+
654+ def get_subject_line (self , loc_context : LocalizationContext ) -> str :
655+ return self ._localize (
656+ loc_context , "subject" , {"author" : self .author .name , "parent_context" : self .parent_context }
657+ )
658+
659+ def build_body (self , builder : EmailBlocksBuilder , loc_context : LocalizationContext ) -> None :
660+ builder .para ("body" , {"author" : self .author .name , "parent_context" : self .parent_context })
661+ builder .user (self .author )
662+ builder .quote (self .markdown_text , markdown = True )
663+ builder .action (self .view_link , "view_action" )
664+
665+ @classmethod
666+ def dummy_data (cls ) -> ThreadReplyEmail :
667+ return ThreadReplyEmail (
668+ user_name = "Alice" ,
669+ author = UserInfo .dummy_bob (),
670+ parent_context = "Best hiking trails near Berlin" ,
671+ markdown_text = "I agree, the Grünewald is **amazing**!" ,
672+ view_link = "https://couchers.org/discussions/123" ,
421673 )
0 commit comments