Skip to main content

max / makenotwork

Ring a bell when a creator asks for a bigger mail allowance b29d7606's last gap, found by reading its done condition against the code rather than assuming work remained. Everything else was built: the window, the counter, the all-or-nothing reservation, all four send paths gated, the creator's count and cap and reset date, the warning band, the form with a reason, the operator queue, the override write. What a filed request did not do was tell anybody. It landed in a table and waited for someone to remember to open /admin/mail-caps. For a creator whose sends are being refused, that is the soft cap behaving like a hard one: the application path is the whole defence of the design, and an application path nobody is told about is not one. send_alert, which is what this server already uses for an operator who needs to know something -- support tickets, monitor status changes, ingested alerts. After the row is written and never a reason to fail the request: the queue holds the ask whether or not the mail arrives, so a failure costs a doorbell rather than a creator's application. support@ and not info@. Role addresses land in one mailbox and the sorting is done on the address, so a precise one is the whole benefit of having several.
Author: Max Johnson <me@maxj.phd> · 2026-08-30 22:42 UTC
Signed with PGP, not checked
Commit: 8d4f29164e6bf02e7fc23c4ee7f7dccb244b1388
Parent: a29f67d
2 files changed, +65 insertions, -25 deletions
M server/Cargo.lock +24 -24
@@ -2875,7 +2875,7 @@
2875 2875 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
2876 2876 dependencies = [
2877 2877 "libc",
2878 - "windows-sys 0.59.0",
2878 + "windows-sys 0.52.0",
2879 2879 ]
2880 2880
2881 2881 [[package]]
@@ -4811,7 +4811,7 @@
4811 4811 source = "registry+https://github.com/rust-lang/crates.io-index"
4812 4812 checksum = "82cb6a9f675da968c63b6208c641b9dca58fc0133ae53375736b1767b0cab8bd"
4813 4813 dependencies = [
4814 - "windows-sys 0.59.0",
4814 + "windows-sys 0.61.2",
4815 4815 ]
4816 4816
4817 4817 [[package]]
@@ -5722,7 +5722,7 @@
5722 5722 source = "registry+https://github.com/rust-lang/crates.io-index"
5723 5723 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
5724 5724 dependencies = [
5725 - "windows-sys 0.59.0",
5725 + "windows-sys 0.61.2",
5726 5726 ]
5727 5727
5728 5728 [[package]]
@@ -6636,7 +6636,7 @@
6636 6636
6637 6637 [[package]]
6638 6638 name = "quasi-axum"
6639 - version = "0.91.0"
6639 + version = "0.91.1"
6640 6640 dependencies = [
6641 6641 "axum",
6642 6642 "http 1.4.2",
@@ -6647,7 +6647,7 @@
6647 6647
6648 6648 [[package]]
6649 6649 name = "quasi-basics"
6650 - version = "0.91.0"
6650 + version = "0.91.1"
6651 6651 dependencies = [
6652 6652 "makeover-layout",
6653 6653 "quasi-router",
@@ -6655,7 +6655,7 @@
6655 6655
6656 6656 [[package]]
6657 6657 name = "quasi-http"
6658 - version = "0.91.0"
6658 + version = "0.91.1"
6659 6659 dependencies = [
6660 6660 "form_urlencoded",
6661 6661 "http 1.4.2",
@@ -6664,7 +6664,7 @@
6664 6664
6665 6665 [[package]]
6666 6666 name = "quasi-router"
6667 - version = "0.91.0"
6667 + version = "0.91.1"
6668 6668 dependencies = [
6669 6669 "makeover-layout",
6670 6670 ]
@@ -6687,7 +6687,7 @@
6687 6687
6688 6688 [[package]]
6689 6689 name = "quasi-webview"
6690 - version = "0.91.0"
6690 + version = "0.91.1"
6691 6691 dependencies = [
6692 6692 "docengine",
6693 6693 "makeover-layout",
@@ -6763,7 +6763,7 @@
6763 6763 "once_cell",
6764 6764 "socket2",
6765 6765 "tracing",
6766 - "windows-sys 0.59.0",
6766 + "windows-sys 0.52.0",
6767 6767 ]
6768 6768
6769 6769 [[package]]
@@ -7326,7 +7326,7 @@
7326 7326 "errno",
7327 7327 "libc",
7328 7328 "linux-raw-sys 0.4.15",
7329 - "windows-sys 0.59.0",
7329 + "windows-sys 0.52.0",
7330 7330 ]
7331 7331
7332 7332 [[package]]
@@ -7339,7 +7339,7 @@
7339 7339 "errno",
7340 7340 "libc",
7341 7341 "linux-raw-sys 0.12.1",
7342 - "windows-sys 0.59.0",
7342 + "windows-sys 0.52.0",
7343 7343 ]
7344 7344
7345 7345 [[package]]
@@ -7419,7 +7419,7 @@
7419 7419 "security-framework 3.7.0",
7420 7420 "security-framework-sys",
7421 7421 "webpki-root-certs",
7422 - "windows-sys 0.59.0",
7422 + "windows-sys 0.52.0",
7423 7423 ]
7424 7424
7425 7425 [[package]]
@@ -8545,7 +8545,7 @@
8545 8545 "getrandom 0.4.3",
8546 8546 "once_cell",
8547 8547 "rustix 1.1.4",
8548 - "windows-sys 0.59.0",
8548 + "windows-sys 0.52.0",
8549 8549 ]
8550 8550
8551 8551 [[package]]
@@ -10759,14 +10759,6 @@
10759 10759 "pkg-config",
10760 10760 ]
10761 10761
10762 - [[patch.unused]]
10763 - name = "synckit-client"
10764 - version = "0.10.0"
10765 -
10766 - [[patch.unused]]
10767 - name = "synckit-config"
10768 - version = "0.2.0"
10769 -
10770 10762 [[patch.unused]]
10771 10763 name = "kberg"
10772 10764 version = "0.1.0"
@@ -10781,11 +10773,11 @@
10781 10773
10782 10774 [[patch.unused]]
10783 10775 name = "quasi-immediate"
10784 - version = "0.91.0"
10776 + version = "0.91.1"
10785 10777
10786 10778 [[patch.unused]]
10787 10779 name = "quasi-notifs"
10788 - version = "0.91.0"
10780 + version = "0.91.1"
10789 10781
10790 10782 [[patch.unused]]
10791 10783 name = "quasi-store"
@@ -10793,4 +10785,12 @@
10793 10785
10794 10786 [[patch.unused]]
10795 10787 name = "quasi-tauri"
10796 - version = "0.91.0"
10788 + version = "0.91.1"
10789 +
10790 + [[patch.unused]]
10791 + name = "synckit-client"
10792 + version = "0.10.0"
10793 +
10794 + [[patch.unused]]
10795 + name = "synckit-config"
10796 + version = "0.2.0"
@@ -17,7 +17,23 @@
17 17 use serde::Deserialize;
18 18 use sqlx::PgPool;
19 19
20 - use crate::{auth::AuthUser, db, error::Result, templates::FormStatusTemplate};
20 + use crate::{
21 + auth::AuthUser, config::Config, db, email::EmailClient, error::Result,
22 + templates::FormStatusTemplate,
23 + };
24 +
25 + /// Where a filed request is announced.
26 + ///
27 + /// The queue at `/admin/mail-caps` is where a request *lives*; this is what
28 + /// tells anybody it arrived. A soft cap's whole defence is that the application
29 + /// path works, and an application path nobody is told about is a page somebody
30 + /// has to remember to open -- which, for a creator whose sends are being
31 + /// refused, is the cap behaving like a hard one after all.
32 + ///
33 + /// The support address rather than `info@`: role addresses land in one mailbox
34 + /// and the sorting is done on the address, so a precise one is the whole
35 + /// benefit of having several.
36 + const OPERATOR: &str = "support@makenot.work";
21 37
22 38 /// Form input for an allowance increase.
23 39 #[derive(Debug, Deserialize)]
@@ -38,6 +54,8 @@
38 54 #[tracing::instrument(skip_all, name = "users::mail_cap_request")]
39 55 pub(in crate::routes::api) async fn mail_cap_request(
40 56 State(db): State<PgPool>,
57 + State(email): State<EmailClient>,
58 + State(config): State<Config>,
41 59 AuthUser(user): AuthUser,
42 60 Form(form): Form<MailCapRequestForm>,
43 61 ) -> Result<Response> {
@@ -96,6 +114,28 @@
96 114 "creator asked for a higher monthly mail allowance"
97 115 );
98 116
117 + // After the row, and never a reason to fail the request. The queue holds
118 + // the ask whether or not this arrives, so a mail failure costs a doorbell
119 + // and not a creator's application.
120 + let announcement = format!(
121 + "{} ({}) asks for {} emails a month, up from {current}.\n\nUser ID: {}\n\nWhy:\n{reason}\n\nDecide at {}/admin/mail-caps",
122 + user.username, user.email, form.requested_cap, user.id, config.host_url,
123 + );
124 + if let Err(error) = email
125 + .send_alert(
126 + OPERATOR,
127 + &format!(
128 + "[mail-cap] {} asks for {}",
129 + user.username, form.requested_cap
130 + ),
131 + &announcement,
132 + )
133 + .await
134 + {
135 + tracing::warn!(error = ?error, user_id = %user.id,
136 + "mail allowance request filed but the operator was not emailed");
137 + }
138 +
99 139 Ok(Html(
100 140 FormStatusTemplate {
101 141 success: true,