[Backend Integration] Mailing List Auto-Subscription
The definitions in this ticket were based off of the discussions in the comments and a Slack thread.
Goal
Build the Django-side integration layer for Boost mailing list subscriptions — covering the subscribe/unsubscribe client, confirmation flow, subscription tracking model, and the mailing list card component wired to live endpoints.
Scope
Full-stack: mailman-core REST API client, confirmation email flow, subscription model, HTMX-powered subscribe card, and confirmation pages.
Acceptance Criteria
Mailman REST API Client
- A thin
mailing_list/client.py wrapper around mailman-core's REST API exposes three operations:
subscribe(email, list_id) — POSTs to /3.1/members with all pre-confirmation flags set to True, treating a 409 (already a member) as a no-op.
unsubscribe(email, list_id) — looks up the member ID then DELETEs /3.1/members/<id>; if the member is not found, discards any pending subscription request so the user can re-subscribe cleanly.
is_confirmed(email, list_id) — GETs /3.1/lists/<list_id>/member/<email> to check active membership.
- All three raise
MailmanAPIError on API failure; callers handle errors gracefully.
- API credentials (
MAILMAN_REST_API_URL, MAILMAN_REST_API_PATH, MAILMAN_REST_API_USER, MAILMAN_REST_API_PASS) and the list of managed lists (MAILMAN_LISTS) are read from Django settings / environment variables.
Subscription Model
- New
UserMailingListSubscription model tracks boost.org-managed subscriptions:
user (FK to AUTH_USER_MODEL), list_id, email, status (pending | active), subscribed_at.
unique_together on (user, list_id).
Mailing List Labels
mailing_list/constants.py defines human-readable labels, addresses, and descriptions for the 3 active lists:
boost.lists.boost.org → "Boost Developers"
boost-announce.lists.boost.org → "Boost Announcements"
boost-users.lists.boost.org → "Boost Users"
Subscription Views & Confirmation Flow
QuickSubscribeView (POST /mailing-list/quick-subscribe/) — single-list subscribe for both authenticated and anonymous users, returning an HTMX outerHTML swap of the mailing list card:
- Authenticated path: creates a
UserMailingListSubscription with status=pending, sends a confirmation email, and renders a pending state. Re-POSTing for an already-pending subscription returns the pending card without re-sending. Already-active subscriptions return the success card.
- Anonymous path: stateless — checks Mailman for an existing subscription, sends a confirmation email, and renders a pending card.
- Returns inline error states (invalid email, invalid list ID, email send failure) as re-renders of the same card partial.
SubscribeView (POST /mailing-list/subscribe/) — authenticated, multi-list subscribe/unsubscribe via the user profile page:
- Computes the delta between currently-subscribed lists and the submitted set.
- Creates
UserMailingListSubscription(status=pending) records and sends a single confirmation email covering all newly-selected lists.
- Unsubscribes from deselected lists (calls
mailman_unsubscribe; pending records are simply deleted without an API call).
- Returns an HTMX fragment (
_subscribe_result.html) listing pending, subscribed, unsubscribed, and failed lists.
ConfirmSubscriptionView (GET /mailing-list/confirm/<token>/) — validates a signed token (Django signing, defaults to 7-day TTL), calls mailman_subscribe for each list in the payload, and updates matching UserMailingListSubscription records to status=active. Renders either confirm_success.html or confirm_invalid.html.
- Confirmation email (
confirm_subscription.txt) includes the list name, address, description, the signed confirmation URL, and the expiry window.
Mailing List Card Component
_mailing_list_card.html partial is fully wired up with three states:
- Default (subscribe form): HTMX
POST to QuickSubscribeView with outerHTML swap; renders inline field errors.
- Pending: shows the email address a confirmation was sent to and a "Manage your lists →" link.
- Subscribed: swaps to
_subscribe_success_card.html showing the subscribed email, the count of active subscriptions, and a "Manage your lists →" link.
MailingListCardMixin (in mailing_list/mixins.py) injects all required context variables into any class-based view that includes the card:
mailing_list_card_subscribe_url, mailing_list_card_list_id, mailing_list_card_login_url
mailing_list_card_is_subscribed, mailing_list_card_state, mailing_list_card_user_email, mailing_list_card_manage_url, mailing_list_card_subscription_count (authenticated users only)
CommunityView and LearnPageView in core/views.py both inherit MailingListCardMixin.
Confirmation Pages
confirm_success.html — lists confirmed and (if any) failed lists by name and address; links back to the homepage.
confirm_invalid.html — shown for expired or tampered tokens; includes the expiry window and a link to the homepage.
FE/BE Actions
| Description |
Endpoint |
| Subscribe (single list, authenticated + anonymous) |
POST /mailing-list/quick-subscribe/ |
| Subscribe / unsubscribe (multi-list, authenticated only) |
POST /mailing-list/subscribe/ |
| Confirm email token |
GET /mailing-list/confirm/<token>/ |
Out of Scope
- Infrastructure: exposing mailman-core REST API to boost.org's server (firewall /
bind_address config) — this is a server-side task for Sam.
- Post-auth subscription modal shown after login/signup.
- Subscription management section in the user profile settings page (toggling lists from the profile).
- Regular import of existing mailman subscriptions for previously subscribed users.
[Backend Integration] Mailing List Auto-Subscription
The definitions in this ticket were based off of the discussions in the comments and a Slack thread.
Goal
Build the Django-side integration layer for Boost mailing list subscriptions — covering the subscribe/unsubscribe client, confirmation flow, subscription tracking model, and the mailing list card component wired to live endpoints.
Scope
Full-stack: mailman-core REST API client, confirmation email flow, subscription model, HTMX-powered subscribe card, and confirmation pages.
Acceptance Criteria
Mailman REST API Client
mailing_list/client.pywrapper around mailman-core's REST API exposes three operations:subscribe(email, list_id)— POSTs to/3.1/memberswith all pre-confirmation flags set toTrue, treating a 409 (already a member) as a no-op.unsubscribe(email, list_id)— looks up the member ID then DELETEs/3.1/members/<id>; if the member is not found, discards any pending subscription request so the user can re-subscribe cleanly.is_confirmed(email, list_id)— GETs/3.1/lists/<list_id>/member/<email>to check active membership.MailmanAPIErroron API failure; callers handle errors gracefully.MAILMAN_REST_API_URL,MAILMAN_REST_API_PATH,MAILMAN_REST_API_USER,MAILMAN_REST_API_PASS) and the list of managed lists (MAILMAN_LISTS) are read from Django settings / environment variables.Subscription Model
UserMailingListSubscriptionmodel tracks boost.org-managed subscriptions:user(FK toAUTH_USER_MODEL),list_id,email,status(pending|active),subscribed_at.unique_togetheron(user, list_id).Mailing List Labels
mailing_list/constants.pydefines human-readable labels, addresses, and descriptions for the 3 active lists:boost.lists.boost.org→ "Boost Developers"boost-announce.lists.boost.org→ "Boost Announcements"boost-users.lists.boost.org→ "Boost Users"Subscription Views & Confirmation Flow
QuickSubscribeView(POST /mailing-list/quick-subscribe/) — single-list subscribe for both authenticated and anonymous users, returning an HTMXouterHTMLswap of the mailing list card:UserMailingListSubscriptionwithstatus=pending, sends a confirmation email, and renders a pending state. Re-POSTing for an already-pending subscription returns the pending card without re-sending. Already-active subscriptions return the success card.SubscribeView(POST /mailing-list/subscribe/) — authenticated, multi-list subscribe/unsubscribe via the user profile page:UserMailingListSubscription(status=pending)records and sends a single confirmation email covering all newly-selected lists.mailman_unsubscribe; pending records are simply deleted without an API call)._subscribe_result.html) listing pending, subscribed, unsubscribed, and failed lists.ConfirmSubscriptionView(GET /mailing-list/confirm/<token>/) — validates a signed token (Djangosigning, defaults to 7-day TTL), callsmailman_subscribefor each list in the payload, and updates matchingUserMailingListSubscriptionrecords tostatus=active. Renders eitherconfirm_success.htmlorconfirm_invalid.html.confirm_subscription.txt) includes the list name, address, description, the signed confirmation URL, and the expiry window.Mailing List Card Component
_mailing_list_card.htmlpartial is fully wired up with three states:POSTtoQuickSubscribeViewwithouterHTMLswap; renders inline field errors._subscribe_success_card.htmlshowing the subscribed email, the count of active subscriptions, and a "Manage your lists →" link.MailingListCardMixin(inmailing_list/mixins.py) injects all required context variables into any class-based view that includes the card:mailing_list_card_subscribe_url,mailing_list_card_list_id,mailing_list_card_login_urlmailing_list_card_is_subscribed,mailing_list_card_state,mailing_list_card_user_email,mailing_list_card_manage_url,mailing_list_card_subscription_count(authenticated users only)CommunityViewandLearnPageViewincore/views.pyboth inheritMailingListCardMixin.Confirmation Pages
confirm_success.html— lists confirmed and (if any) failed lists by name and address; links back to the homepage.confirm_invalid.html— shown for expired or tampered tokens; includes the expiry window and a link to the homepage.FE/BE Actions
POST /mailing-list/quick-subscribe/POST /mailing-list/subscribe/GET /mailing-list/confirm/<token>/Out of Scope
bind_addressconfig) — this is a server-side task for Sam.