No description
  • Python 97.6%
  • Vim Snippet 0.9%
  • Dockerfile 0.8%
  • Shell 0.7%
Find a file
bogac 199d77b840 Merge docs into README and AGENTS; git deploy on celen
- README.md is the single reference: status, tools (trash via labels
  allowed and documented), access, Google Cloud, settings, git deploy,
  backups, scheduled tasks, tests, troubleshooting, open items
- AGENTS.md holds agent rules; CLAUDE.md includes it
- Remove SETUP.md, CALENDARS.md, HANDOVER.md, docs/mcp-server-reference.md
- .env.example: CALDAV_URL empty by default so calendars stay off
- Point Caddyfile.snippet and authorize.py at the README

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:38:49 +11:00
.dockerignore Initial commit: bogacai-mcp mail server, calendar code (not deployed), handover 2026-10-24 00:36:03 +11:00
.env.example Merge docs into README and AGENTS; git deploy on celen 2026-10-28 22:38:49 +11:00
.gitignore Initial commit: bogacai-mcp mail server, calendar code (not deployed), handover 2026-10-24 00:36:03 +11:00
AGENTS.md Merge docs into README and AGENTS; git deploy on celen 2026-10-28 22:38:49 +11:00
authorize.py Merge docs into README and AGENTS; git deploy on celen 2026-10-28 22:38:49 +11:00
Caddyfile.snippet Merge docs into README and AGENTS; git deploy on celen 2026-10-28 22:38:49 +11:00
calendars.py Initial commit: bogacai-mcp mail server, calendar code (not deployed), handover 2026-10-24 00:36:03 +11:00
CLAUDE.md Merge docs into README and AGENTS; git deploy on celen 2026-10-28 22:38:49 +11:00
docker-compose.yml Initial commit: bogacai-mcp mail server, calendar code (not deployed), handover 2026-10-24 00:36:03 +11:00
Dockerfile Initial commit: bogacai-mcp mail server, calendar code (not deployed), handover 2026-10-24 00:36:03 +11:00
mailbox Initial commit: bogacai-mcp mail server, calendar code (not deployed), handover 2026-10-24 00:36:03 +11:00
mailboxes.py Initial commit: bogacai-mcp mail server, calendar code (not deployed), handover 2026-10-24 00:36:03 +11:00
README.md Merge docs into README and AGENTS; git deploy on celen 2026-10-28 22:38:49 +11:00
requirements.txt Initial commit: bogacai-mcp mail server, calendar code (not deployed), handover 2026-10-24 00:36:03 +11:00
server.py Initial commit: bogacai-mcp mail server, calendar code (not deployed), handover 2026-10-24 00:36:03 +11:00

bogacai-mcp

A small MCP server on celen that Claude uses as a custom connector named bogacai-mcp. It lets Claude search, read, draft, send and label mail in several Gmail mailboxes, and (once switched on) read and add to the Nextcloud calendars. Built 9 October 2026 instead of using ready-made connectors: Claude's own Gmail connector holds one Google account at a time, and this way no third party holds the mailbox access.

This README is the single reference for the server. Celen-wide facts (Caddy, ports table, backups) live in server-docs/celen-server-documentation.md, which points back here. Rules for agents working in this repo are in AGENTS.md.

Claude (Anthropic's cloud)
  -> https://mcp.celen.cloud      Caddy on celen (native process, TLS)
  -> 127.0.0.1:8811               container gmail-mcp, port 8000 inside
  -> Gmail API, using a stored approval per mailbox
  -> Nextcloud CalDAV             (calendar tools, off until CALDAV_URL is set)
Thing Value
Address given to Claude https://mcp.celen.cloud/mcp
Connector name in Claude bogacai-mcp
Google Cloud app bogacai-mail, External, unverified on purpose
Server folder ~/gmail-mcp on celen (note: not mcp)
Container gmail-mcp, built locally from python:3.13-slim
Source Forgejo bogac/mcp, Mac checkout ~/dev/forgejo/mcp
Mailboxes ada = bogachancelen.ai@gmail.com, vera = veracelen.ai@gmail.com, both approved 9 October 2026
Allowed sign-ins (ALLOWED_EMAILS) the same two addresses

1. Status

Checked on celen on 28 October 2026:

  • Running and healthy. ~/gmail-mcp is a git checkout of bogac/mcp, deployed by git pull since 28 October 2026 (before that, files copied in by hand on 9 October 2026)
  • Mail tools on, sending on, calendars off (CALDAV_URL not set)
  • .env mode 600, tokens mode 600
  • Searches answered in both mailboxes on 23 October 2026
  • The pre-git folder is kept as ~/gmail-mcp-old-2026-10-28 for rollback (open item 10)

2. Tools

Mail (always on)

Every tool except list_accounts takes the mailbox name, so it is always explicit which mailbox is used. The server tells Claude to ask when the mailbox was not named.

Tool What it does Changes anything?
list_accounts Mailbox names and the address behind each No
search_emails Gmail search syntax (from:, is:unread, newer_than:7d). Sender, recipients, subject, date, labels, preview. 10 results by default, 50 at most No
read_email One full message: headers, text, attachment names, types and sizes. Text beyond 40,000 characters is cut and flagged No
read_thread Every message in a conversation, oldest first No
create_draft Saves a plain-text draft, optionally as a reply that stays in its conversation Adds a draft
send_email Sends at once. To, cc, bcc, plain text, optionally a reply. Refused when ALLOW_SEND=false Sends mail
list_labels The mailbox's labels, user labels first No
modify_labels Adds or removes existing labels on one message, by name or id Changes labels; see below

What modify_labels can do, because it accepts any existing label, system labels included:

  • remove INBOX archives; remove UNREAD marks read; add STARRED stars
  • add TRASH moves the message to Trash, and add SPAM marks it as spam. Google's labels guide lists both as labels that "can be manually applied" through messages.modify, which is the call this tool makes. Gmail empties Trash after 30 days. The Ada weekday brief relies on this. Read from the code and Google's docs; not yet tried on a real message

What the server cannot do: delete mail permanently (the Google permission gmail.modify does not allow it), open or send attachments, send formatted mail, or keep any mail (nothing is stored after a request). Searching does not mark mail read (checked on ada, 9 October 2026).

Calendars (off until CALDAV_URL is set)

Tool What it does Changes anything?
list_calendars Calendars and task lists, and which ones may be changed No
query_events Events between two dates, repeating events expanded, local time. At most 62 days per call No
query_tasks Open tasks in one list, optionally only those due by a date No
create_task New task. Refused without notes, because every task needs a first step Adds a task
update_task Changes an open task's title, notes or due date. Refuses completed or cancelled tasks Edits a task
create_event New event, 30 minutes if no end is given Adds an event

Only calendars named in CALDAV_WRITE_CALENDARS can be changed; everything else, including the MBS subscription, is read-only. No tool completes, reopens or deletes anything.

Claude asks before using a tool that changes something, unless that tool is set to "always allow" in Claude. The server itself does not ask.

3. How access works

Two independent locks. Keep them apart when reasoning about access.

  1. Who may connect. Claude signs in through Google. FastMCP's OAuthProxy sits in front; AllowlistVerifier (a GoogleTokenVerifier subclass) accepts a token only if Google says the address is verified, the address is in ALLOWED_EMAILS, and the token was issued to this Google client. Results are cached for 300 seconds. AuthMiddleware repeats the allowlist check on every tool call. Only Claude's callbacks https://claude.ai/api/mcp/auth_callback and https://claude.com/api/mcp/auth_callback can complete a sign-in (override with ALLOWED_REDIRECT_URIS), so another MCP client cannot use the gate. The server issues its own tokens to Claude; a raw Google token sent to /mcp is refused.
  2. Which mailboxes exist. A mailbox exists only after a one-time approval on the server (section 7). The approval is stored encrypted in data/tokens/<name>.token. Being on ALLOWED_EMAILS does not make an account a mailbox, and a mailbox does not need to be on ALLOWED_EMAILS.

Every allowed sign-in can use every mailbox. There is no per-mailbox permission.

Three paths answer without sign-in and show nothing private: / (home page), /privacy, and /healthz (returns a static ok). Google will not publish an External app without the first two, which is why the server serves them.

4. Google Cloud

One project, one app, one OAuth client, all at https://console.cloud.google.com.

Item Value
API enabled Gmail API, not "Gmail MCP API" (a separate Google product this does not use)
App name bogacai-mail, audience External
Client type Web application
Redirect URIs https://mcp.celen.cloud/auth/callback (Claude's sign-in, lock 1) and http://localhost:8765/ (mailbox approval, lock 2; the trailing slash matters)
Scopes openid, https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/gmail.modify
Branding home page https://mcp.celen.cloud/, privacy policy https://mcp.celen.cloud/privacy, authorized domain celen.cloud
Publishing status Must be In production. While "Testing", Google cancels every approval after seven days

The status has not been read off the Audience page. It is confirmed by behavior: both mailboxes were approved on 9 October 2026 and both answered a search on 23 October 2026, 14 days later.

The app is unverified on purpose. Google's help says a personal-use app under 100 users needs no verification; the cost is a "Google hasn't verified this app" warning at each approval. Google keeps showing "Your app requires verification". Do not submit it.

Every approval and sign-in makes Google send a "Security alert" or "You shared some Google Account data" email to that account. Expected.

5. Files and settings

In this repo

File Role
server.py Sign-in gate, the mail tools, the calendar tools (registered only when CALDAV_URL is set), the public pages
mailboxes.py Encrypted mailbox store: one Fernet-encrypted JSON file per mailbox, mode 600, reloaded when its modification time changes
authorize.py One-time mailbox approval, run through ./mailbox
calendars.py Nextcloud CalDAV client for the calendar tools
mailbox Wrapper: docker compose run --rm -p 127.0.0.1:8765:8765 gmail-mcp python authorize.py "$@"
Dockerfile, docker-compose.yml, requirements.txt Build and run. Every Python dependency pinned (fastmcp==4.1.0, which uses httpx2, not httpx)
.env.example Every setting, blank. The real .env exists only on the server
Caddyfile.snippet The mcp.celen.cloud block, with the optional IP restriction commented out
.gitignore, .dockerignore Keep .env, data/ and __pycache__/ out of git; .dockerignore also keeps *.md and .git/ out of the image

On the server, in ~/gmail-mcp

The repo checkout plus two things that never leave the server: .env, and data/ (bind-mounted to /data) with data/tokens/<name>.token per mailbox and data/fastmcp/ for Claude's encrypted sign-in state. .env and data/ only work together: data/ is encrypted with keys from .env.

Backups (checked 28 October 2026): the nightly system-config tar (backup_system_config.sh) holds .env and docker-compose.yml. Nothing backs up data/. Code is in Forgejo.

Settings (.env)

After any change: docker compose up -d --force-recreate. A plain restart does not re-read env_file.

Setting What it controls
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET The Google client (secret)
ALLOWED_EMAILS Who may connect, commas between
BASE_URL https://mcp.celen.cloud
JWT_SIGNING_KEY Secret. Signs Claude's sessions and keys data/fastmcp. Changing it signs Claude out everywhere
TOKEN_ENCRYPTION_KEY Secret. Decrypts data/tokens. Changing or losing it means approving every mailbox again
ALLOW_SEND true or false
HOST_PORT Loopback port Caddy forwards to, 8811
PUID, PGID User the container runs as, 1000:1000
APP_NAME, CONTACT_EMAIL Shown on the home and privacy pages. Empty, so the pages say "Private mail connector"
CALDAV_URL Nextcloud calendar home. Any non-empty value switches the calendar tools on
CALDAV_USER, CALDAV_PASSWORD Nextcloud user and an app password (secret), never the real password
CALDAV_TZ Default Australia/Melbourne
CALDAV_READ_CALENDARS Optional allowlist of display names; empty means every calendar
CALDAV_WRITE_CALENDARS The only calendars that can be changed, display names, case-insensitive
LOG_LEVEL, AUTHORIZE_PORT Optional, defaults INFO and 8765

./mailbox --gen-secrets prints a fresh JWT_SIGNING_KEY and TOKEN_ENCRYPTION_KEY.

6. Deploying

Edit on the Mac, push to Forgejo, pull on celen. Never edit code on the server.

[Mac]    cd ~/dev/forgejo/mcp && git push
[server] cd ~/gmail-mcp && git pull --ff-only && docker compose up -d --build
[server] sleep 10 && docker compose ps && docker compose logs --tail 20

Check after every deploy:

  • git log -1 --format=%h on the server matches the Mac
  • the log shows Allowed sign-ins: ..., Mailboxes: ada, vera, Sending is on|off and Calendars: on|off. A Configuration problem: ... line names the setting to fix
  • curl http://127.0.0.1:8811/healthz prints ok. The server needs a few seconds to start; a curl in the same second gets "connection reset"
  • if the tool list changed, start a fresh Claude session; if the new tools still do not show, disconnect and reconnect the connector in Claude's settings

The server pulls with a read-only Forgejo deploy key, ~/.ssh/mcp_deploy, through the forgejo-mcp host alias in ~/.ssh/config (same pattern as forgejo-manec).

Fresh install (rebuild from nothing)

  1. DNS: an IONOS A record for mcp.celen.cloud pointing at celen's public IP (new subdomains default to the wrong one).
  2. Google Cloud as in section 4, then Audience > Publish app.
  3. On celen:
    [server] git clone forgejo-mcp:bogac/mcp.git ~/gmail-mcp && cd ~/gmail-mcp
    [server] mkdir data && cp .env.example .env && chmod 600 .env   # create data/ yourself so root does not own it
    [server] docker compose build && ./mailbox --gen-secrets
    
    Fill .env: the two generated secrets, the Google client id and secret, ALLOWED_EMAILS.
  4. docker compose up -d, then the checks above.
  5. Add the Caddyfile.snippet block to /etc/caddy/Caddyfile, sudo systemctl reload caddy, and curl https://mcp.celen.cloud/healthz from anywhere.
  6. Approve each mailbox (section 7).
  7. In Claude: Settings > Connectors > Add custom connector, name bogacai-mcp, address https://mcp.celen.cloud/mcp. Sign in with an address from ALLOWED_EMAILS, then ask Claude to list the Gmail accounts.

The connector lives in your Claude account, not on the Mac, so it works from any device.

7. Day to day (server, in ~/gmail-mcp)

Task Command
Logs: one line per tool call (tool, mailbox, signed-in address) and per refused sign-in; never content docker compose logs -f
Health docker compose ps should say "healthy" (checked every 60 seconds)
List mailboxes ./mailbox --list
Add a mailbox see below
Rename a mailbox mv data/tokens/<old>.token data/tokens/<new>.token; no restart, no re-approval
Remove a mailbox ./mailbox --remove <name>, then remove the app at myaccount.google.com/connections in that account
Change who may connect edit ALLOWED_EMAILS, then docker compose up -d --force-recreate
Turn sending off or on edit ALLOW_SEND, then docker compose up -d --force-recreate
Sign Claude out everywhere ./mailbox --gen-secrets, paste only the new JWT_SIGNING_KEY, then docker compose up -d --force-recreate

Adding a mailbox. The approval needs a browser and the server has none, so the Mac's browser is borrowed through a tunnel:

[Mac]    ssh -L 8765:127.0.0.1:8765 celen
[server] cd ~/gmail-mcp && ./mailbox <name>      # in that same session

Open the printed link on the Mac, pick the Gmail account, click Advanced on the unverified-app warning, continue, allow. The terminal prints Stored mailbox '<name>' for <address>, and the server picks it up on the next request. Names are lowercase letters, digits, - and _. A university or company Google account may block unverified apps ("Access blocked"); such a mailbox cannot be added this way.

8. Calendars: switching them on

About 15 minutes. Not done yet.

  1. App password. Nextcloud: Settings, Security, Devices & sessions, "Create new app password", name it mcp-calendars. Store it in Vaultwarden. Revoking it later switches calendar access off.
  2. Settings. Add the calendar block from .env.example to ~/gmail-mcp/.env: CALDAV_URL with your Nextcloud user name, CALDAV_USER, CALDAV_PASSWORD, and CALDAV_WRITE_CALENDARS (for example ADOD,Boğaç, matching the display names in Nextcloud).
  3. Recreate. docker compose up -d --force-recreate; the log must say Calendars: on, writable: ....
  4. Hourly subscription refresh. Nextcloud refreshes subscribed calendars (MBS) once a day by default:
    [server] docker exec -u www-data nextcloud-app-1 php occ config:app:set dav calendarSubscriptionRefreshRate --value "PT1H"
    
  5. First real run. Ask Claude to list the calendars with bogacai-mcp, then query a week of events and check the MBS meetings appear with titles.

Nextcloud calendar URLs use the URI slug, not the display name. List the slugs with docker exec -u www-data nextcloud-app-1 php occ dav:list-calendars bogac.

9. Hardening

  • Caddy IP restriction. The commented @outsiders rule in Caddyfile.snippet limits /mcp to 160.79.104.0/21, the range Anthropic publishes for its outbound calls. The sign-in pages must stay open because your own browser visits them. Not enabled on celen (checked 28 October 2026). To try it: copy the rule into the live block, sudo systemctl reload caddy, ask Claude to list the accounts; if that fails, take it out again.
  • Container. Read-only root, /tmp in memory, all capabilities dropped, no-new-privileges, non-root, port on loopback only, FastMCP update check off. Stateless HTTP transport, so restarts do not break Claude's session; Claude's sign-in survives a restart (tested 9 October 2026).

10. Security: what protects what

If this happens What protects you The limit
A stranger finds the address Only /, /privacy and /healthz answer without sign-in The address is public
Someone signs in with their own Google account Refused on every request None found in testing
Another program tries to use the gate Only Claude's callback addresses complete a sign-in None found in testing
Someone copies data/ Encrypted, useless without .env A backup holding both is as sensitive as the server
The server is taken over Nothing: whoever has .env and data/ can use both mailboxes Remove the app at myaccount.google.com/connections in each account; that cancels the approvals at once
An email carries text written to steer an assistant Claude asks before a changing action; ALLOW_SEND=false removes sending Claude can still be misled, and can trash mail through modify_labels (recoverable for 30 days)
A flaw in the program Hardened container (section 9) It can still reach data/ and Google

The mail passes through Claude, so Anthropic's systems see what you ask about. True of any mail connector.

11. Scheduled tasks that use this connector

A scheduled task keeps the connector list it had when it was created, and Cowork's edit screen has no connector setting. Connector tools in scheduled runs can arrive deferred and need one tool search to load (anthropics/claude-ai-mcp #999). State read from the trigger API on 28 October 2026:

Task ID Enabled Connector attached? Instructions
Ada weekday brief trig_016Qz9B7WBmmvD4GPUig7Agq Yes, weekdays 07:00 Melbourne No Say to use bogacai-mcp, mailbox ada, and trash handled mail by adding TRASH
Vera weekday brief trig_01XiWXh5rGR9CnB982jsjeH6 No (since 24 October 2026) No Predate the server; name no connector
Ada weekday brief (cloud) trig_01KfpqpSe7RLQnZdr2XrLrix Yes, but no schedule Yes Placeholder, "not configured yet"

The other Ada and Vera tasks (Sunday briefs, Friday wraps, meeting capture) are disabled and have no connector.

12. Tests

The suites used before deployment are lost (they lived in a previous agent's scratch space). What they covered, so they can be rebuilt in a tests/ folder:

  • Tools, 29 checks: FastMCP's in-memory Client against create_server(None), with mailboxes.gmail patched to return a Gmail client on googleapiclient.http.HttpMockSequence. Encrypted store round-trip, mode 600, wrong key refused; search parsing; Turkish text in windows-1254; HTML-only bodies; truncation at 40,000 characters; threads; replies keeping threadId, In-Reply-To and References; draft versus send; ALLOW_SEND=false; label resolution; bad mailbox names; Gmail 404; revoked approval (RefreshError).
  • Sign-in gate, 28 checks: the real app under uvicorn, a fake Google token endpoint, and Google's tokeninfo and userinfo stubbed through httpx2.MockTransport passed to build_auth() (its keyword arguments exist only for this). Full flow: client registration, /authorize, consent with CSRF token, /auth/callback, /token with PKCE, /mcp. No token, garbage token, raw Google token, listed, unlisted and unverified addresses, a token from another Google client, a non-Claude redirect, the public pages, and encrypted login state on disk.
  • Calendars, 23 checks (15 October 2026): against Radicale 3.8.3. Repeating and all-day events, Turkish names, open and completed tasks, the due filter, create and update, the 30-minute default, every refusal. Never run against real Nextcloud; the open question is how Nextcloud presents the MBS subscription.

Exercised against real Gmail: sign-in, list_accounts, search_emails in both mailboxes. Not yet: read_email, read_thread, drafting, sending, label changes including TRASH.

13. Troubleshooting

What you see Cause Fix
Claude answers with the wrong mailbox or no ada/vera Claude's built-in Gmail connector is on in that chat and was used instead Switch it off in the chat's connector menu
Claude asks to connect again Signed in with an unlisted address, or JWT_SIGNING_KEY changed Connect again with an allowed address
"Google no longer accepts the stored approval for mailbox X" Approval cancelled: "Testing" status, password change, six months unused, or app removed ./mailbox X again
"No mailbox named X. Available: ..." Wrong name Use a listed name
"Sending is switched off on this server" ALLOW_SEND=false Change it, or draft instead
"Mailbox X cannot be decrypted" TOKEN_ENCRYPTION_KEY changed Restore the old key, or approve again
docker compose ps says "Restarting" A setting is missing or wrong docker compose logs --tail 20, look for Configuration problem:
curl "connection reset" right after up -d Still starting Wait 10 seconds
TLS error right after a Caddy change Certificate still being issued Wait a minute, then check DNS
Google Error 400: redirect_uri_mismatch Client lacks exactly http://localhost:8765/ Fix the redirect list, wait a few minutes
"Port 8765 is busy" Previous approval's socket not released Wait a minute
Google's Publish button is grey Home or privacy link missing on Branding Fill in section 4's links
Search preview shows &#39; Gmail returns snippet HTML-escaped and it is passed through Not fixed yet: html.unescape in parse_message
Scheduled run: "bogacai-mcp isn't available in this session" Task created before the connector existed Section 11
Interactive run: list_accounts blocked as "Exfil Scouting" Claude's auto-approval classifier saw an unattended call returning addresses Authorize the call in the task's own instructions; do not route around it
Calendars: "Nextcloud refused the calendar list (401)" Wrong user or app password Section 8, steps 1 and 2
Calendars: "Could not reach Nextcloud: ConnectTimeout" Container cannot reach celen.cloud through the router Add extra_hosts: ["celen.cloud:host-gateway"] under gmail-mcp: in docker-compose.yml, commit, deploy
MBS meetings have no titles Outlook feed published as free/busy only Republish from Outlook with full details
MBS a day behind Hourly refresh not set Section 8, step 4

14. Design decisions

  • Separate locks for connecting and for mailboxes, so a mailbox never needs to be a login.
  • No permanent delete. gmail.modify cannot do it. Trashing through modify_labels is allowed on purpose (decided 28 October 2026): it is recoverable for 30 days and the Ada brief uses it.
  • ALLOW_SEND switch: one setting turns sending off for every mailbox; drafting stays on.
  • The server serves its own home and privacy pages, because Google requires both links on a domain you own before publishing.
  • Unverified Google app in production. Do not submit it for verification.
  • Stateless HTTP transport, so restarts do not break Claude's session.
  • House pattern on celen: loopback port, Caddy terminates TLS, one Compose network per service, secrets only in .env.
  • Calendars are opt-in and write only to named calendars, with no complete, reopen or delete.

15. Open items

As of 28 October 2026, most important first.

  1. Ada weekday brief has no connector. It runs every weekday but cannot reach bogacai-mcp. Recreate it with the connector attached (section 11), then retire the old trigger and the cloud placeholder.
  2. Vera weekday brief. Disabled; its instructions predate the server. Rewrite with the connector attached, or delete.
  3. Live tests. Try read_email, read_thread, a draft, a send to yourself, and a label change including TRASH on a message of your own (section 12).
  4. Back up data/. Not in any backup (section 5). Losing it costs two mailbox approvals and a fresh Claude sign-in, no mail.
  5. Secrets in Vaultwarden. Confirm GOOGLE_CLIENT_SECRET, JWT_SIGNING_KEY and TOKEN_ENCRYPTION_KEY are stored there.
  6. Uptime Kuma monitor for https://mcp.celen.cloud/healthz. None exists.
  7. Caddy IP restriction. Try it (section 9).
  8. &#39; in search previews. html.unescape in parse_message (section 13).
  9. Rebuild the tests in a tests/ folder (section 12).
  10. Delete ~/gmail-mcp-old-2026-10-28 once a later deploy by git pull has worked.
  11. Calendars. Switch on when wanted (section 8).
  12. Sending. ALLOW_SEND is true. Decide whether it stays on.

16. Decisions not made

  • WhatsApp. Asked for, then dropped on 14 October 2026. If it returns: the official route needs a second number not on WhatsApp (Meta's Cloud API, 24-hour free-text window, templates otherwise); the unofficial route on his own number risks a ban and must not share a container with mail. He does not use Zapier.

Sources