Skip to content

feat: email passive checks (spec 02) - #27

Merged
fclairamb merged 11 commits into
mainfrom
feat/email-passive-checks
May 2, 2026
Merged

fclairamb merged 11 commits into
mainfrom
feat/email-passive-checks

Conversation

@fclairamb

Copy link
Copy Markdown
Owner

Summary

Implements spec 02 — email passive checks: a new email check type where a unique per-check email address (<48-hex-token>@<inbox-domain>) is monitored via the JMAP supervisor wired up in spec 01. Inbound mail is turned into check results the same way heartbeat pings are.

  • New checkemail package mirrors checkheartbeat: 24-byte random token in config, ErrNotExecutable on Execute, registered in the checker registry.
  • Worker now dispatches passive checks (heartbeat + email) through a single executePassiveJob path; output messages are parameterized on a noun ("Heartbeat" / "Email").
  • New db.Service.GetCheckByEmailToken (postgres + sqlite) plus a partial index migration so the JMAP handler can resolve a token to a check globally.
  • New internal/handlers/emailcheck/ JMAP handler. Recipient regex captures the token and an optional +status plus-suffix; status priority is plus-addressing > X-SolidPing-Status header > [STATUS] subject tag > default up. Token-shaped recipients with no matching check are OutcomeRejected so they move to Processed.
  • App wires the handler into the JMAP manager during bootstrap. The manager idles when no email_inbox system parameter is configured, so this is safe in all deployments.

Test plan

  • make build-backend passes
  • make lint-back clean (0 issues with golangci-lint v2.12.0)
  • go test ./internal/db -run TestPostgresService passes (new GetByEmailToken subtest covers happy path + miss)
  • go test ./internal/checkers/checkemail/... passes
  • go test ./internal/handlers/emailcheck/... 11 tests covering token extraction priority, header/subject fallbacks, multi-recipient scan
  • go test ./internal/checkworker/... passes against renamed executePassiveJob
  • Smoke test against a real JMAP server with an actual email send (manual, post-merge)

🤖 Generated with Claude Code

fclairamb and others added 10 commits May 2, 2026 03:40
Step 2 of email-passive-checks: mirror checkheartbeat structure.
EmailConfig holds a 48-char hex token (24 random bytes from
crypto/rand). EmailChecker.Validate auto-generates the token if
missing and fills sane name/slug defaults. Execute returns
ErrNotExecutable — the worker will treat email checks passively
the same way it treats heartbeats.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 3 of email-passive-checks: hook checkemail into the factory
switches so spec/config parsing finds it. Same single-line pattern
as every other check type.
Step 4 of email-passive-checks: rename executeHeartbeatJob →
executePassiveJob and dispatch on isPassiveCheckType so email
checks share the same overdue/grace-period logic. The result
output noun ("Heartbeat" / "Email") is parameterized on the
check type — the rest of the messages are protocol-agnostic.
Step 5 of email-passive-checks: the JMAP email handler needs to
resolve a recipient token to a check across all orgs. Tokens are
24 random bytes (48 hex chars) so collisions are negligible — no
org scoping is required.

Postgres uses config->>'token'; sqlite uses json_extract. Both
get a partial index on (token) WHERE type = 'email' AND
deleted_at IS NULL so the lookup stays cheap as the checks table
grows. Mock service in notifications gets the new method too.
Step 6 of email-passive-checks: a jmap.Handler implementation
that turns inbound emails into check results.

Recipient resolution: regex on the local-part for the 48-hex
token, with an optional +status suffix (token+down@…). Status
priority is plus-addressing > X-SolidPing-Status header >
[STATUS] subject tag > default "up". Cc/Bcc are scanned in
addition to To. Token-shaped recipients with no matching check
return OutcomeRejected so the email moves to Processed and the
chain stops; non-token recipients return OutcomeIgnored to let
other handlers try.

Once a check is resolved, recordResult mirrors the heartbeat
service: SaveResultWithStatusTracking + ProcessCheckResult,
skipping incident processing for "running". Output keeps the
envelope (from, subject, messageId, receivedAt) for debugging.
Step 7 of email-passive-checks: register the email-check handler
on the jmap.Manager during server bootstrap. The manager idles
until the email_inbox system parameter is configured, so adding
this handler is safe in all deployments — without a JMAP server
configured nothing changes.
Spec 02 implemented across 7 commits on this branch:
checkemail package, registry hook, worker passive
generalization, db lookup + migration, JMAP handler, app
wiring. Backend builds, lints clean, tests pass in isolation.
@fclairamb
fclairamb enabled auto-merge (squash) May 2, 2026 02:02
@fclairamb
fclairamb merged commit ba5536f into main May 2, 2026
10 checks passed
@fclairamb
fclairamb deleted the feat/email-passive-checks branch May 2, 2026 02:13
@fclairamb fclairamb mentioned this pull request May 2, 2026
fclairamb added a commit that referenced this pull request Jun 23, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.1.0](v0.0.0...v0.1.0)
(2026-06-22)


### Features

* add check type registry, sample configs, notification senders, and
observability integrations
([eea049e](eea049e))
* batch — Warning/Degraded statuses, SSL/Docker check upgrades, MCP
OAuth 2.1, config-as-code
([#85](#85))
([936d830](936d830))
* batch spec implementations (dashboard UI, jobs, notifications, SaaS,
perf) ([#78](#78))
([fe003e4](fe003e4))
* bulk feature integration (discovery, web push, badges, webhooks, and
more) ([#73](#73))
([a976856](a976856))
* custom user-agent, refresh checks, vite plugin switch, CI updates
([#1](#1))
([cc00858](cc00858))
* documentation site served at /docs (co-located in web/docs)
([#88](#88))
([27d9ccd](27d9ccd))
* email check frontend (spec 03)
([#30](#30))
([aebd106](aebd106))
* email inbox foundation via JMAP (spec 01)
([#24](#24))
([0f3b0c1](0f3b0c1))
* email passive checks (spec 02)
([#27](#27))
([ba5536f](ba5536f))
* **entitlements:** trim to MaxSSOUsers + MaxChecksPerMinute + status
page history fixes
([#50](#50))
([a007de0](a007de0))
* **entitlements:** trim to MaxSSOUsers + MaxChecksPerMinute, enforce
both ([#49](#49))
([d25cc5f](d25cc5f))
* group incident correlation (spec 04 backend v1)
([#31](#31))
([1883986](1883986))
* **observability:** instrument hot path + add bench harness
([#60](#60))
([c6604cc](c6604cc))
* **oncall:** searchable timezone dropdown + sidebar logo polish
([#46](#46))
([4396898](4396898))
* per-IP HTTP rate limiting and concurrency limiting
([#59](#59))
([f9cc273](f9cc273))
* soften HTTP rate limits with bounded queues + request timeout
([#63](#63))
([025501b](025501b))
* SolidPing — distributed uptime monitoring platform
([eef4383](eef4383))
* **statuspages:** drag-and-drop resource reordering + dash conventions
([#41](#41))
([cf910d4](cf910d4))


### Bug Fixes

* **deps:** update dependency i18next to v26
([#26](#26))
([d5d1b7e](d5d1b7e))
* **deps:** update dependency lucide-react to v1
([#28](#28))
([cdc8e8c](cdc8e8c))
* **deps:** update dependency react-i18next to v17
([#29](#29))
([13135d8](13135d8))
* **deps:** update dependency recharts to v3.8.1
([#14](#14))
([5d95a3b](5d95a3b))
* **deps:** update go dependencies (non-major)
([#16](#16))
([2450f13](2450f13))
* **deps:** update go dependencies (non-major)
([#19](#19))
([f842310](f842310))
* **deps:** update go dependencies (non-major)
([#37](#37))
([c020eca](c020eca))
* **deps:** update go dependencies (non-major)
([#44](#44))
([ff0bcd0](ff0bcd0))
* **deps:** update go dependencies (non-major)
([#52](#52))
([2e3bc28](2e3bc28))
* **deps:** update go dependencies (non-major)
([#69](#69))
([a65ed4c](a65ed4c))
* **deps:** update go dependencies (non-major)
([#77](#77))
([a0920ff](a0920ff))
* **deps:** update go dependencies (non-major) to v1.4.2
([#86](#86))
([1eb7504](1eb7504))
* **deps:** update module github.com/aws/aws-sdk-go-v2/config to
v1.32.18 ([#72](#72))
([6406537](6406537))
* **deps:** update module github.com/aws/aws-sdk-go-v2/service/s3 to
v1.101.0 ([#39](#39))
([f9c9563](f9c9563))
* **deps:** update module github.com/go-webauthn/webauthn to v0.17.3
([#45](#45))
([593931a](593931a))
* **deps:** update module github.com/go-webauthn/webauthn to v0.17.4
([#70](#70))
([fca1c97](fca1c97))
* **deps:** update module github.com/ibm/sarama to v1.48.1
([#47](#47))
([93ccf3a](93ccf3a))
* **deps:** update module github.com/ibm/sarama to v1.48.2
([#54](#54))
([e84d868](e84d868))
* **deps:** update module github.com/ibm/sarama to v1.49.0
([#64](#64))
([fd61bd7](fd61bd7))
* **deps:** update module github.com/oapi-codegen/runtime to v1.4.1
([#65](#65))
([a3a3e9f](a3a3e9f))
* **deps:** update module github.com/prometheus/common to v0.67.5
([#61](#61))
([095dd6f](095dd6f))
* **deps:** update module github.com/slack-go/slack to v0.24.0
([#75](#75))
([6e86d5b](6e86d5b))
* **deps:** update module golang.org/x/sys to v0.44.0
([#42](#42))
([a245730](a245730))
* **deps:** update module golang.org/x/sys to v0.45.0
([#67](#67))
([49c3d90](49c3d90))
* **deps:** update module golang.org/x/term to v0.43.0
([#43](#43))
([4a934f1](4a934f1))
* **deps:** update module google.golang.org/grpc to v1.81.0
([#36](#36))
([1be183a](1be183a))
* **deps:** update module google.golang.org/grpc to v1.81.1
([#55](#55))
([6bfac2d](6bfac2d))
* status lifecycle improvements and created/running result handling
([#5](#5))
([fc64c7d](fc64c7d))
* **status0:** white page header and de-duplicated footer
([#48](#48))
([77506a7](77506a7))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
This was referenced Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant