Coverage for app / backend / src / couchers / notifications / render_email.py: 85%

242 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-19 14:14 +0000

1import logging 

2from dataclasses import dataclass 

3from typing import Any 

4 

5from couchers import urls 

6from couchers.i18n import LocalizationContext 

7from couchers.i18n.localize import format_phone_number 

8from couchers.models import Notification, NotificationTopicAction 

9from couchers.notifications.quick_links import ( 

10 can_unsubscribe_topic_key, 

11 generate_quick_decline_link, 

12 generate_unsub_topic_action, 

13 generate_unsub_topic_key, 

14) 

15from couchers.proto import api_pb2, notification_data_pb2 

16from couchers.utils import now, to_aware_datetime 

17 

18logger = logging.getLogger(__name__) 

19 

20 

21@dataclass(kw_only=True) 

22class RenderedEmailNotification: 

23 # email subject 

24 subject: str 

25 # shows up when listing emails in many clients 

26 preview: str 

27 # corresponds to .mjml + .txt file in templates/v2 

28 template_name: str 

29 # other template args 

30 template_args: dict[str, Any] 

31 # the link label on the topic_action unsubscribe link 

32 topic_action_unsubscribe_text: str | None = None 

33 # the link label on the topic_key unsubscribe link 

34 topic_key_unsubscribe_text: str | None = None 

35 

36 

37def render_email_notification( 

38 notification: Notification, loc_context: LocalizationContext 

39) -> RenderedEmailNotification: 

40 data = notification.topic_action.data_type.FromString(notification.data) # type: ignore[attr-defined] 

41 if notification.topic == "host_request": 

42 view_link = urls.host_request(host_request_id=data.host_request.host_request_id) 

43 if notification.action == "missed_messages": 

44 their_your = "their" if data.am_host else "your" 

45 other = data.user 

46 # "declined your host request", or similar 

47 message = f"{other.name} sent you message(s) in {their_your} host request" 

48 return RenderedEmailNotification( 

49 subject=message, 

50 preview=message, 

51 template_name="host_request__plain", 

52 template_args={ 

53 "view_link": view_link, 

54 "host_request": data.host_request, 

55 "message": message, 

56 "other": UserTemplateArgs.from_protobuf_user(other), 

57 }, 

58 topic_action_unsubscribe_text="missed messages in host requests", 

59 ) 

60 elif notification.action == "create": 

61 other = data.surfer 

62 message = f"{other.name} sent you a host request" 

63 return RenderedEmailNotification( 

64 subject=message, 

65 preview=message, 

66 template_name="host_request__new", 

67 template_args={ 

68 "view_link": view_link, 

69 "quick_decline_link": generate_quick_decline_link(data.host_request), 

70 "host_request": data.host_request, 

71 "message": message, 

72 "other": UserTemplateArgs.from_protobuf_user(other), 

73 "text": data.text, 

74 }, 

75 topic_action_unsubscribe_text="new host requests", 

76 ) 

77 elif notification.action == "message": 77 ↛ 78line 77 didn't jump to line 78 because the condition on line 77 was never true

78 other = data.user 

79 if data.am_host: 

80 message = f"{other.name} sent you a message in their host request" 

81 else: 

82 message = f"{other.name} sent you a message in your host request" 

83 topic_action_unsub_text = "messages in host request" 

84 return RenderedEmailNotification( 

85 subject=message, 

86 preview=message, 

87 template_name="host_request__message", 

88 template_args={ 

89 "view_link": view_link, 

90 "host_request": data.host_request, 

91 "message": message, 

92 "other": UserTemplateArgs.from_protobuf_user(other), 

93 "text": data.text, 

94 }, 

95 topic_action_unsubscribe_text=topic_action_unsub_text, 

96 ) 

97 elif notification.action in ["accept", "reject", "confirm", "cancel"]: 

98 if notification.action in ["accept", "reject"]: 

99 other = data.host 

100 their_your = "your" 

101 else: 

102 other = data.surfer 

103 their_your = "their" 

104 actioned = { 

105 "accept": "accepted", 

106 "reject": "declined", 

107 "confirm": "confirmed", 

108 "cancel": "cancelled", 

109 }[notification.action] 

110 # "declined your host request", or similar 

111 message = f"{other.name} {actioned} {their_your} host request" 

112 return RenderedEmailNotification( 

113 subject=message, 

114 preview=message, 

115 template_name="host_request__plain", 

116 template_args={ 

117 "view_link": view_link, 

118 "host_request": data.host_request, 

119 "message": message, 

120 "other": UserTemplateArgs.from_protobuf_user(other), 

121 }, 

122 topic_action_unsubscribe_text=f"{actioned} host requests", 

123 ) 

124 elif notification.action == "reminder": 124 ↛ 743line 124 didn't jump to line 743 because the condition on line 124 was always true

125 message = f"You have a pending host request from {data.surfer.name}!" 

126 description = "Please respond to the request!" 

127 return RenderedEmailNotification( 

128 subject=message, 

129 preview=description, 

130 template_name="host_request__plain", 

131 template_args={ 

132 "view_link": view_link, 

133 "host_request": data.host_request, 

134 "message": description, 

135 "other": UserTemplateArgs.from_protobuf_user(data.surfer), 

136 }, 

137 topic_action_unsubscribe_text="Pending host request reminders", 

138 ) 

139 elif notification.topic_action == NotificationTopicAction.password__change: 

140 title = "Your password was changed" 

141 message = "Your login password for Couchers.org was changed." 

142 return RenderedEmailNotification( 

143 subject=title, 

144 preview=message, 

145 template_name="security", 

146 template_args={ 

147 "title": title, 

148 "message": message, 

149 }, 

150 ) 

151 elif notification.topic_action == NotificationTopicAction.password_reset__start: 

152 message = "Someone initiated a password change on your account." 

153 return RenderedEmailNotification( 

154 subject="Reset your Couchers.org password", 

155 preview=message, 

156 template_name="password_reset", 

157 template_args={ 

158 "password_reset_link": urls.password_reset_link(password_reset_token=data.password_reset_token) 

159 }, 

160 ) 

161 elif notification.topic_action == NotificationTopicAction.password_reset__complete: 

162 title = "Your password was successfully reset" 

163 message = "Your password on Couchers.org was changed. If that was you, then no further action is needed." 

164 return RenderedEmailNotification( 

165 subject=title, 

166 preview=title, 

167 template_name="security", 

168 template_args={ 

169 "title": title, 

170 "message": message, 

171 }, 

172 ) 

173 elif notification.topic_action == NotificationTopicAction.email_address__change: 

174 title = "An email change was initiated on your account" 

175 message = f"An email change to the email <b>{data.new_email}</b> was initiated on your account." 

176 return RenderedEmailNotification( 

177 subject=title, 

178 preview=title, 

179 template_name="security", 

180 template_args={ 

181 "title": title, 

182 "message": message, 

183 }, 

184 ) 

185 elif notification.topic_action == NotificationTopicAction.email_address__verify: 

186 title = "Email change completed" 

187 message = "Your new email address has been verified." 

188 return RenderedEmailNotification( 

189 subject=title, 

190 preview=message, 

191 template_name="security", 

192 template_args={ 

193 "title": title, 

194 "message": message, 

195 }, 

196 ) 

197 elif notification.topic_action == NotificationTopicAction.phone_number__change: 

198 title = "Phone verification started" 

199 message = f"You started phone number verification with the number <b>{format_phone_number(data.phone)}</b>." 

200 return RenderedEmailNotification( 

201 subject=title, 

202 preview=message, 

203 template_name="security", 

204 template_args={ 

205 "title": title, 

206 "message": message, 

207 }, 

208 ) 

209 elif notification.topic_action == NotificationTopicAction.phone_number__verify: 

210 title = "Phone successfully verified" 

211 message = f"Your phone was successfully verified as <b>{format_phone_number(data.phone)}</b> on Couchers.org." 

212 message_plain = f"Your phone was successfully verified as {format_phone_number(data.phone)} on Couchers.org." 

213 return RenderedEmailNotification( 

214 subject=title, 

215 preview=message_plain, 

216 template_name="security", 

217 template_args={ 

218 "title": title, 

219 "message": message, 

220 }, 

221 ) 

222 elif notification.topic_action == NotificationTopicAction.gender__change: 

223 title = "Your gender was changed" 

224 message = f"Your gender on Couchers.org was changed to <b>{data.gender}</b> by an admin." 

225 message_plain = f"Your gender on Couchers.org was changed to {data.gender} by an admin." 

226 return RenderedEmailNotification( 

227 subject=title, 

228 preview=message_plain, 

229 template_name="security", 

230 template_args={ 

231 "title": title, 

232 "message": message, 

233 }, 

234 ) 

235 elif notification.topic_action == NotificationTopicAction.birthdate__change: 

236 title = "Your date of birth was changed" 

237 birthdate = loc_context.localize_date_from_iso(data.birthdate) 

238 message = f"Your date of birth on Couchers.org was changed to <b>{birthdate}</b> by an admin." 

239 message_plain = f"Your date of birth on Couchers.org was changed to {birthdate} by an admin." 

240 return RenderedEmailNotification( 

241 subject=title, 

242 preview=message_plain, 

243 template_name="security", 

244 template_args={ 

245 "title": title, 

246 "message": message, 

247 }, 

248 ) 

249 elif notification.topic_action == NotificationTopicAction.api_key__create: 

250 return RenderedEmailNotification( 

251 subject="Your API key for Couchers.org", 

252 preview="We have issued you an API key as per your request.", 

253 template_name="api_key", 

254 template_args={ 

255 "api_key": data.api_key, 

256 "expiry": data.expiry, 

257 }, 

258 ) 

259 elif notification.topic_action.display in ["badge:add", "badge:remove"]: 

260 actioned = "added to" if notification.action == "add" else "removed from" 

261 title = f"The {data.badge_name} badge was {actioned} your profile" 

262 return RenderedEmailNotification( 

263 subject=title, 

264 preview=title, 

265 template_name="badge", 

266 template_args={ 

267 "badge_name": data.badge_name, 

268 "actioned": actioned, 

269 "unsub_type": "badge additions" if notification.action == "add" else "badge removals", 

270 }, 

271 topic_action_unsubscribe_text="badge additions" if notification.action == "add" else "badge removals", 

272 ) 

273 elif notification.topic_action == NotificationTopicAction.donation__received: 

274 title = loc_context.localize_string("notifications.donation_received.title") 

275 message = loc_context.localize_string( 

276 "notifications.donation_received.thanks_amount", 

277 substitutions={ 

278 "amount": data.amount, 

279 }, 

280 ) 

281 return RenderedEmailNotification( 

282 subject=title, 

283 preview=message, 

284 template_name="donation_received", 

285 template_args={ 

286 "amount": data.amount, 

287 "receipt_url": data.receipt_url, 

288 }, 

289 topic_action_unsubscribe_text="donations received", 

290 ) 

291 elif notification.topic_action == NotificationTopicAction.friend_request__create: 

292 other = data.other_user 

293 preview = f"You've received a friend request from {other.name}" 

294 return RenderedEmailNotification( 

295 subject=f"{other.name} wants to be your friend on Couchers.org!", 

296 preview=preview, 

297 template_name="friend_request", 

298 template_args={ 

299 "friend_requests_link": urls.friend_requests_link(), 

300 "other": UserTemplateArgs.from_protobuf_user(other), 

301 }, 

302 topic_action_unsubscribe_text="new friend requests", 

303 ) 

304 elif notification.topic_action == NotificationTopicAction.friend_request__accept: 

305 other = data.other_user 

306 title = f"{other.name} accepted your friend request!" 

307 preview = f"{other.name} has accepted your friend request" 

308 return RenderedEmailNotification( 

309 subject=title, 

310 preview=preview, 

311 template_name="friend_request_accepted", 

312 template_args={ 

313 "other": UserTemplateArgs.from_protobuf_user(other), 

314 }, 

315 topic_action_unsubscribe_text="accepted friend requests", 

316 ) 

317 elif notification.topic_action == NotificationTopicAction.account_deletion__start: 

318 return RenderedEmailNotification( 

319 subject="Confirm your Couchers.org account deletion", 

320 preview="Please confirm that you want to delete your Couchers.org account.", 

321 template_name="account_deletion_start", 

322 template_args={ 

323 "deletion_link": urls.delete_account_link(account_deletion_token=data.deletion_token), 

324 }, 

325 ) 

326 elif notification.topic_action == NotificationTopicAction.account_deletion__complete: 

327 title = "Your Couchers.org account has been deleted" 

328 return RenderedEmailNotification( 

329 subject=title, 

330 preview="We have deleted your Couchers.org account, to undo, follow the link in this email.", 

331 template_name="account_deletion_complete", 

332 template_args={ 

333 "undelete_link": urls.recover_account_link(account_undelete_token=data.undelete_token), 

334 "days": data.undelete_days, 

335 }, 

336 ) 

337 elif notification.topic_action == NotificationTopicAction.account_deletion__recovered: 

338 title = "Your Couchers.org account has been recovered!" 

339 subtitle = "We have recovered your Couchers.org account as per your request! Welcome back!" 

340 return RenderedEmailNotification( 

341 subject=title, 

342 preview=subtitle, 

343 template_name="account_deletion_recovered", 

344 template_args={ 

345 "app_link": urls.app_link(), 

346 }, 

347 ) 

348 elif notification.topic_action == NotificationTopicAction.chat__message: 

349 return RenderedEmailNotification( 

350 subject=data.message, 

351 preview="You received a message on Couchers.org!", 

352 template_name="chat_message", 

353 template_args={ 

354 "author": UserTemplateArgs.from_protobuf_user(data.author), 

355 "message": data.message, 

356 "text": data.text, 

357 "view_link": urls.chat_link(chat_id=data.group_chat_id), 

358 }, 

359 topic_action_unsubscribe_text="new chat messages", 

360 topic_key_unsubscribe_text="this chat (mute)", 

361 ) 

362 elif notification.topic_action == NotificationTopicAction.chat__missed_messages: 

363 return RenderedEmailNotification( 

364 subject="You have unseen messages on Couchers.org!", 

365 preview="You missed some messages on the platform.", 

366 template_name="chat_unseen_messages", 

367 template_args={ 

368 "items": [ 

369 { 

370 "author": UserTemplateArgs.from_protobuf_user(item.author), 

371 "message": item.message, 

372 "text": item.text, 

373 "view_link": urls.chat_link(chat_id=item.group_chat_id), 

374 } 

375 for item in data.messages 

376 ] 

377 }, 

378 topic_action_unsubscribe_text="unseen chat messages", 

379 ) 

380 elif notification.topic == "event": 

381 event = data.event 

382 start_time = loc_context.localize_datetime(event.start_time) 

383 end_time = loc_context.localize_datetime(event.end_time) 

384 time_display = f"{start_time} - {end_time}" 

385 event_link = urls.event_link(occurrence_id=event.event_id, slug=event.slug) 

386 if notification.action in ["create_approved", "create_any"]: 

387 # create_approved = invitation, approved by mods 

388 # create_any = new event created by anyone (no need for approval) -- off by default 

389 if notification.action == "create_approved": 389 ↛ 392line 389 didn't jump to line 392 because the condition on line 389 was always true

390 subject = f'{data.inviting_user.name} invited you to "{event.title}"' 

391 start_text = "You've been invited to a new event" 

392 elif notification.action == "create_any": 

393 subject = f'{data.inviting_user.name} created an event called "{event.title}"' 

394 start_text = "A new event was created" 

395 community_link = ( 

396 urls.community_link(node_id=data.in_community.community_id, slug=data.in_community.slug) 

397 if data.in_community 

398 else None 

399 ) 

400 return RenderedEmailNotification( 

401 subject=subject, 

402 preview=f"{start_text} on Couchers.org!", 

403 template_name="event_create", 

404 template_args={ 

405 "inviting_user": UserTemplateArgs.from_protobuf_user(data.inviting_user), 

406 "time_display": time_display, 

407 "start_text": start_text, 

408 "nearby": "nearby" if data.nearby else None, 

409 "community": data.in_community if data.in_community else None, 

410 "community_link": community_link, 

411 "event": event, 

412 "view_link": event_link, 

413 }, 

414 topic_action_unsubscribe_text=( 

415 "new events by community members" 

416 if notification.action == "create_any" 

417 else "invitations to events (approved by moderators)" 

418 ), 

419 ) 

420 elif notification.action == "update": 

421 updated_text = ", ".join(data.updated_items) 

422 return RenderedEmailNotification( 

423 subject=f'{data.updating_user.name} updated "{event.title}"', 

424 preview="An event you are subscribed to was updated.", 

425 template_name="event_update", 

426 template_args={ 

427 "updating_user": UserTemplateArgs.from_protobuf_user(data.updating_user), 

428 "time_display": time_display, 

429 "event": event, 

430 "updated_text": updated_text, 

431 "view_link": event_link, 

432 }, 

433 topic_action_unsubscribe_text="event updates", 

434 ) 

435 elif notification.action == "cancel": 

436 return RenderedEmailNotification( 

437 subject=f'{data.cancelling_user.name} cancelled "{event.title}"', 

438 preview="An event you are subscribed to has been cancelled.", 

439 template_name="event_cancel", 

440 template_args={ 

441 "cancelling_user": UserTemplateArgs.from_protobuf_user(data.cancelling_user), 

442 "time_display": time_display, 

443 "event": event, 

444 "view_link": event_link, 

445 }, 

446 topic_action_unsubscribe_text="event cancellations", 

447 ) 

448 elif notification.action == "delete": 448 ↛ 449line 448 didn't jump to line 449 because the condition on line 448 was never true

449 return RenderedEmailNotification( 

450 subject=f'A moderator deleted "{event.title}"', 

451 preview="An event you are subscribed to has been deleted.", 

452 template_name="event_delete", 

453 template_args={ 

454 "time_display": time_display, 

455 "event": event, 

456 }, 

457 topic_action_unsubscribe_text="event deletions", 

458 ) 

459 elif notification.action == "invite_organizer": 459 ↛ 460line 459 didn't jump to line 460 because the condition on line 459 was never true

460 return RenderedEmailNotification( 

461 subject=f'{data.inviting_user.name} invited you to co-organize "{event.title}"', 

462 preview="You were invited to co-organize an event on Couchers.org.", 

463 template_name="event_invite_organizer", 

464 template_args={ 

465 "inviting_user": UserTemplateArgs.from_protobuf_user(data.inviting_user), 

466 "time_display": time_display, 

467 "event": event, 

468 "view_link": event_link, 

469 }, 

470 topic_action_unsubscribe_text="invitations to co-organize events", 

471 ) 

472 elif notification.action == "comment": 

473 return RenderedEmailNotification( 

474 subject=f'{data.author.name} commented on "{event.title}"', 

475 preview="Someone commented on an event you are attending.", 

476 template_name="event_comment", 

477 template_args={ 

478 "author": UserTemplateArgs.from_protobuf_user(data.author), 

479 "time_display": time_display, 

480 "event": event, 

481 "content": data.reply.content, 

482 "view_link": event_link, 

483 }, 

484 topic_action_unsubscribe_text="event comments", 

485 ) 

486 elif notification.action == "reminder": 486 ↛ 743line 486 didn't jump to line 743 because the condition on line 486 was always true

487 return RenderedEmailNotification( 

488 subject=f'Reminder: "{data.event.title}" starts soon', 

489 preview="Don't forget your upcoming event on Couchers.org", 

490 template_name="event_reminder", 

491 template_args={ 

492 "time_display": time_display, 

493 "event": event, 

494 "view_link": event_link, 

495 }, 

496 topic_action_unsubscribe_text="event reminders", 

497 ) 

498 elif notification.topic == "discussion": 

499 discussion = data.discussion 

500 discussion_link = urls.discussion_link(discussion_id=discussion.discussion_id, slug=discussion.slug) 

501 if notification.action == "create": 501 ↛ 502line 501 didn't jump to line 502 because the condition on line 501 was never true

502 return RenderedEmailNotification( 

503 subject=f'{data.author.name} created a discussion: "{discussion.title}"', 

504 preview="Someone created a discussion in a community or group you are subscribed to.", 

505 template_name="discussion_create", 

506 template_args={ 

507 "author": UserTemplateArgs.from_protobuf_user(data.author), 

508 "discussion": discussion, 

509 "view_link": discussion_link, 

510 }, 

511 topic_action_unsubscribe_text="new discussions", 

512 ) 

513 elif notification.action == "comment": 513 ↛ 743line 513 didn't jump to line 743 because the condition on line 513 was always true

514 return RenderedEmailNotification( 

515 subject=f'{data.author.name} commented on "{discussion.title}"', 

516 preview="Someone commented on your discussion.", 

517 template_name="discussion_comment", 

518 template_args={ 

519 "author": UserTemplateArgs.from_protobuf_user(data.author), 

520 "discussion": discussion, 

521 "reply": data.reply, 

522 "view_link": discussion_link, 

523 }, 

524 topic_action_unsubscribe_text="discussion comments", 

525 ) 

526 elif notification.topic_action == NotificationTopicAction.thread__reply: 

527 parent = data.WhichOneof("reply_parent") 

528 if parent == "event": 

529 title = data.event.title 

530 view_link = urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug) 

531 elif parent == "discussion": 531 ↛ 535line 531 didn't jump to line 535 because the condition on line 531 was always true

532 title = data.discussion.title 

533 view_link = urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug) 

534 else: 

535 raise Exception("Can only do replies to events and discussions") 

536 

537 return RenderedEmailNotification( 

538 subject=f'{data.author.name} replied in "{title}"', 

539 preview="Someone replied in a comment thread you have participated in.", 

540 template_name="comment_reply", 

541 template_args={ 

542 "author": UserTemplateArgs.from_protobuf_user(data.author), 

543 "title": title, 

544 "reply": data.reply, 

545 "view_link": view_link, 

546 }, 

547 topic_action_unsubscribe_text="comment replies", 

548 ) 

549 elif notification.topic == "reference": 

550 if notification.action == "receive_friend": 

551 title = f"You've received a friend reference from {data.from_user.name}!" 

552 return RenderedEmailNotification( 

553 subject=title, 

554 preview=data.text, 

555 template_name="friend_reference", 

556 template_args={ 

557 "from_user": UserTemplateArgs.from_protobuf_user(data.from_user), 

558 "profile_references_link": urls.profile_references_link(), 

559 "text": data.text, 

560 }, 

561 topic_action_unsubscribe_text="new references from friends", 

562 ) 

563 elif notification.action in ["receive_hosted", "receive_surfed"]: 

564 title = f"You've received a reference from {data.from_user.name}!" 

565 # what was my type? i surfed with them if i received a "hosted" request 

566 surfed = notification.action == "receive_hosted" 

567 leave_reference_link = urls.leave_reference_link( 

568 reference_type="surfed" if surfed else "hosted", 

569 to_user_id=data.from_user.user_id, 

570 host_request_id=data.host_request_id, 

571 ) 

572 profile_references_link = urls.profile_references_link() 

573 if data.text: 573 ↛ 574line 573 didn't jump to line 574 because the condition on line 573 was never true

574 preview = data.text 

575 else: 

576 preview = "Please go and write a reference for them too. It's a nice gesture and helps us build a community together!" 

577 return RenderedEmailNotification( 

578 subject=title, 

579 preview=preview, 

580 template_name="host_reference", 

581 template_args={ 

582 "from_user": UserTemplateArgs.from_protobuf_user(data.from_user), 

583 "leave_reference_link": leave_reference_link, 

584 "profile_references_link": profile_references_link, 

585 "text": data.text, 

586 "both_written": True if data.text else False, 

587 "surfed": surfed, 

588 }, 

589 topic_action_unsubscribe_text="new references from " + ("hosts" if surfed else "surfers"), 

590 ) 

591 elif notification.action in ["reminder_hosted", "reminder_surfed"]: 591 ↛ 743line 591 didn't jump to line 743 because the condition on line 591 was always true

592 # what was my type? i surfed with them if i get a surfed reminder 

593 surfed = notification.action == "reminder_surfed" 

594 leave_reference_link = urls.leave_reference_link( 

595 reference_type="surfed" if surfed else "hosted", 

596 to_user_id=data.other_user.user_id, 

597 host_request_id=data.host_request_id, 

598 ) 

599 title = f"You have {data.days_left} days to write a reference for {data.other_user.name}!" 

600 preview = "It's a nice gesture to write references and helps us build a community together! References will become visible 2 weeks after the stay, or when you've both written a reference for each other, whichever happens first." 

601 return RenderedEmailNotification( 

602 subject=title, 

603 preview=preview, 

604 template_name="reference_reminder", 

605 template_args={ 

606 "other_user": UserTemplateArgs.from_protobuf_user(data.other_user), 

607 "leave_reference_link": leave_reference_link, 

608 "days_left": str(data.days_left), 

609 "surfed": surfed, 

610 }, 

611 topic_action_unsubscribe_text=("surfed" if surfed else "hosted") + " reference reminders", 

612 ) 

613 elif notification.topic_action == NotificationTopicAction.onboarding__reminder: 

614 if notification.key == "1": 

615 return RenderedEmailNotification( 

616 subject="Welcome to Couchers.org and the future of couch surfing", 

617 preview="We are so excited to have you join our community!", 

618 template_name="onboarding1", 

619 template_args={ 

620 "app_link": urls.app_link(), 

621 "edit_profile_link": urls.edit_profile_link(), 

622 }, 

623 topic_action_unsubscribe_text="onboarding emails", 

624 ) 

625 elif notification.key == "2": 625 ↛ 743line 625 didn't jump to line 743 because the condition on line 625 was always true

626 return RenderedEmailNotification( 

627 subject="Complete your profile on Couchers.org", 

628 preview="We would ask one big favour of you: please fill out your profile by adding a photo and some text.", 

629 template_name="onboarding2", 

630 template_args={ 

631 "edit_profile_link": urls.edit_profile_link(), 

632 }, 

633 topic_action_unsubscribe_text="onboarding emails", 

634 ) 

635 elif notification.topic_action == NotificationTopicAction.modnote__create: 

636 title = "You have received a mod note" 

637 message = "You have received an important note from the moderators. You must read and acknowledge it before continuing to use the platform." 

638 return RenderedEmailNotification( 

639 subject=title, 

640 preview=message, 

641 template_name="mod_note", 

642 template_args={"title": title}, 

643 ) 

644 elif notification.topic_action == NotificationTopicAction.verification__sv_success: 

645 title = "Strong Verification succeeded" 

646 message = "You have been verified with Strong Verification! You will now see a tick next to your name on the platform." 

647 return RenderedEmailNotification( 

648 subject=title, 

649 preview=message, 

650 template_name="strong_verification_success", 

651 template_args={ 

652 "message": message, 

653 }, 

654 ) 

655 elif notification.topic_action == NotificationTopicAction.verification__sv_fail: 

656 title = "Strong Verification failed" 

657 if data.reason == notification_data_pb2.SV_FAIL_REASON_WRONG_BIRTHDATE_OR_GENDER: 657 ↛ 658line 657 didn't jump to line 658 because the condition on line 657 was never true

658 reason_message = "The date of birth or gender on your profile does not match the date of birth or sex on your passport. Please contact the support team to update your date of birth or gender, or if your passport sex does not match your gender identity." 

659 elif data.reason == notification_data_pb2.SV_FAIL_REASON_NOT_A_PASSPORT: 

660 reason_message = "You tried to verify with a document that is not a passport. You can only use a passport for Strong Verification." 

661 elif data.reason == notification_data_pb2.SV_FAIL_REASON_DUPLICATE: 661 ↛ 664line 661 didn't jump to line 664 because the condition on line 661 was always true

662 reason_message = "You tried to verify with a passport that has already been used for verification. Please use another passport." 

663 else: 

664 raise Exception("Shouldn't get here") 

665 return RenderedEmailNotification( 

666 subject=title, 

667 preview=title, 

668 template_name="security", 

669 template_args={ 

670 "title": title, 

671 "message": reason_message, 

672 }, 

673 ) 

674 elif notification.topic == "postal_verification": 

675 if notification.action == "postcard_sent": 675 ↛ 687line 675 didn't jump to line 687 because the condition on line 675 was always true

676 title = "Your verification postcard is on its way" 

677 message = f"We've sent a postcard with your verification code to {data.city}, {data.country}. It should arrive within 1-3 weeks depending on your location. Once it arrives, enter the code on the platform to complete verification." 

678 return RenderedEmailNotification( 

679 subject=title, 

680 preview=message, 

681 template_name="security", 

682 template_args={ 

683 "title": title, 

684 "message": message, 

685 }, 

686 ) 

687 elif notification.action == "success": 

688 title = "Postal Verification succeeded" 

689 message = "You have been verified with Postal Verification! Your address has been confirmed." 

690 return RenderedEmailNotification( 

691 subject=title, 

692 preview=message, 

693 template_name="security", 

694 template_args={ 

695 "title": title, 

696 "message": message, 

697 }, 

698 ) 

699 elif notification.action == "failed": 

700 title = "Postal Verification failed" 

701 if data.reason == notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_CODE_EXPIRED: 

702 reason_message = "Your verification code has expired. Codes are valid for 90 days after the postcard is sent. You can start a new verification attempt." 

703 elif data.reason == notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_TOO_MANY_ATTEMPTS: 

704 reason_message = "Too many incorrect code attempts. You can start a new verification attempt." 

705 else: 

706 reason_message = ( 

707 "Your postal verification attempt has failed. You can start a new verification attempt." 

708 ) 

709 return RenderedEmailNotification( 

710 subject=title, 

711 preview=title, 

712 template_name="security", 

713 template_args={ 

714 "title": title, 

715 "message": reason_message, 

716 }, 

717 ) 

718 elif notification.topic_action == NotificationTopicAction.activeness__probe: 

719 title = "Are you still open to hosting on Couchers.org?" 

720 return RenderedEmailNotification( 

721 subject=title, 

722 preview=title, 

723 template_name="activeness_probe", 

724 template_args={ 

725 "app_link": urls.app_link(), 

726 "days_left": (to_aware_datetime(data.deadline) - now()).days, 

727 }, 

728 ) 

729 elif notification.topic_action == NotificationTopicAction.general__new_blog_post: 729 ↛ 743line 729 didn't jump to line 743 because the condition on line 729 was always true

730 title = f"New blog post: {data.title}" 

731 return RenderedEmailNotification( 

732 subject=title, 

733 preview=data.blurb, 

734 template_name="new_blog_post", 

735 template_args={ 

736 "title": data.title, 

737 "blurb": data.blurb, 

738 "url": data.url, 

739 }, 

740 topic_action_unsubscribe_text="new blog post alerts", 

741 ) 

742 

743 raise NotImplementedError(f"Unknown topic-action: {notification.topic}:{notification.action}") 

744 

745 

746def get_list_unsubscribe_header(notification: Notification) -> str | None: 

747 if notification.topic_action.is_critical: 

748 return None 

749 

750 # We can only have one List-Unsubscribe header. 

751 # Prefer topic-key unsubscription as it is more specific than topic-action (e.g. current chat, not all chats). 

752 list_unsubscribe_url: str 

753 if can_unsubscribe_topic_key(notification.topic): 

754 list_unsubscribe_url = generate_unsub_topic_key(notification) 

755 else: 

756 list_unsubscribe_url = generate_unsub_topic_action(notification) 

757 

758 return f"<{list_unsubscribe_url}>" 

759 

760 

761@dataclass(frozen=True, slots=True, kw_only=True) 

762class UserTemplateArgs: 

763 """ 

764 A user's information for email template placeholders. 

765 Allows decoupling from protocol buffer objects. 

766 """ 

767 

768 name: str 

769 age: int 

770 city: str 

771 avatar_url: str 

772 profile_url: str 

773 

774 @staticmethod 

775 def from_protobuf_user(user: api_pb2.User) -> UserTemplateArgs: 

776 return UserTemplateArgs( 

777 name=user.name, 

778 age=user.age, 

779 city=user.city, 

780 avatar_url=user.avatar_thumbnail_url or urls.icon_url(), 

781 profile_url=urls.user_link(username=user.username), 

782 )