| Sign-in | Passwords hashed with bcrypt (cost 10). Sessions are JWTs signed with JWT_SECRET, valid 30 days, sent as Authorization: Bearer. |
| Proving the phone | Either an OTP from Twilio Verify (SMS channel only, never calls), or an incoming JOIN text whose sender number Twilio reports. |
| JOIN codes | 6 characters from an alphabet without look-alikes, single use, 10-minute expiry, optionally tied to the number typed on the page. |
| Twilio webhook | Every request must carry a valid X-Twilio-Signature (HMAC-SHA1 over the public URL and sorted form fields with the auth token), compared in constant time. Unsigned requests get 403. At most 5 JOIN texts per number per hour. |
| Internal calls | The mail service is reachable only on the compose network. Every call needs X-Internal-Token (constant-time compare) and names the user in X-User-Id; only the api knows the token. nginx returns 404 for /api/internal/. |
| Exposure | The api listens on 127.0.0.1 in production; PostgreSQL publishes no ports; the mail service's SMTP-in port is on 127.0.0.1 only. Only nginx (80/443) and Postfix (25) are public. |
| Transport | TLS by Let's Encrypt; HSTS; X-Content-Type-Options: nosniff; Referrer-Policy: same-origin. |
| Privacy in the data | Bcc recipients are visible only to the sender and to themselves; reads are always filtered through the reader's own pointers. |
| Aliases | Unique across users; names of ordinary mailboxes on the same server are reserved. |
| Password change | Needs the current password, unless the account has none yet or the person signed in with an SMS code in the last 15 minutes (the "forgot password" path). |
| Email | Postfix refuses unknown recipients during the SMTP conversation (no backscatter). Mail claiming to come from a PhoneMail number but arriving from outside is rejected. Outgoing mail is DKIM-signed; SPF and DMARC (p=quarantine) are published. |
| Webmail ports | IMAP, submission and the password endpoint listen only on the Docker bridge address, for Roundcube; failed logins wait one second. Submission only sends as the signed-in address. |
| Secrets | .env (600) and /etc/phonemail/twilio.env (640) stay on the Pi, never in the repository or these docs. |
| Brute force | The Pi's fail2ban protects SSH, IMAP, SMTP login and webmail; PhoneMail's own login does not yet have a per-number lockout (see Roadmap). |