DocumentationSearch docs
  1. 01Quick start
  2. 02The checks
  3. 03The verdict
  4. 04MCP setup
  5. 05Monitoring
  6. 06API
  7. 07CLI
  8. 08Self-hosting

Self-hosting

Copy Markdown

Self-hosting

The SendScore app, sendscore/sendscore-app, is open source under AGPL-3.0. It is one Go binary that receives mail over SMTP, runs the engine on it, and serves the reports, the test address pages, the API, and the MCP connector at /api/mcp. To point the local sendscore mcp server at your app, set SENDSCORE_URL to its address and SENDSCORE_TOKEN to an API token from its Account page.

What it needs

  • Docker, or Go 1.26 or later to build it yourself.
  • Port 25, inbound. Newsletter platforms deliver to port 25 and nowhere else. Many cloud and home connections block it, so check before you start.
  • A domain whose MX record points at the server. Test addresses are t-<token>@ that domain.
  • SQLite, which is built in. The database is one file.

The first two are the hard part. If you can’t receive mail on port 25 at a domain of your own, the CLI still runs every check on a saved .eml file, with no server. There is no hosted version, but if you would use one, say so on this issue.

Run it with Docker

The app’s repository has a Dockerfile and a compose.yaml.

git clone https://github.com/sendscore/sendscore-app.git
cd sendscore-app
docker build -t sendscore-app .

Put the configuration below in a .env file, then start it.

docker run -d --name sendscore --env-file .env --stop-timeout 90 \
  -p 25:2525 -p 127.0.0.1:3014:3014 \
  -v sendscore-data:/data \
  -v /srv/sendscore/certs:/certs:ro \
  sendscore-app

Or run docker compose up -d --build, which does the same from compose.yaml. Uncomment the certificate mount there first, since it ships commented out and the app exits if the files it is told to load are missing.

  • The image is small and runs as a user with no privileges (uid 65532), with no shell inside.
  • Port 25 on the host goes to 2525 in the container, since a user with no privileges cannot listen on 25.
  • The database is /data/sendscore.db on the sendscore-data volume. Copying the file of a running database can give a torn copy, so back up with docker compose exec app sendscore-app backup /data/backup.db.
  • GET /healthz answers 200 when the database, the heartbeat, and the SMTP listener are working, and 503 otherwise. The image’s HEALTHCHECK uses it.
  • Publish the web port on 127.0.0.1 only, and put a web server with TLS in front of it.
  • Mount the directory that holds the mail domain’s certificate, not the files, so a renewed certificate is picked up. Set SENDSCORE_TLS_CERT and SENDSCORE_TLS_KEY to the files inside it, such as /certs/fullchain.pem and /certs/privkey.pem. The key must be readable by uid 65532, so have your certificate tool copy both files there after each renewal.
  • Stop it with a timeout of 90 seconds, as above, because open sessions get up to 70 seconds to finish and background work up to 15 more.

Build and run without Docker

git clone https://github.com/sendscore/sendscore-app.git
cd sendscore-app
go build -o sendscore-app .

To update, run git pull, then build again.

Then set the configuration below and run the binary. With nothing set, it serves the web app on 127.0.0.1:3014 and receives mail on 127.0.0.1:2525, for addresses at @localhost, which is enough to try it on your own machine.

Configuration

Set these in the environment, or in a .env.local or .env file in the directory the app starts in.

Variable Default What it sets
SENDSCORE_ADDR 127.0.0.1:3014 Where the web app and API listen
SENDSCORE_SMTP_ADDR 127.0.0.1:2525 Where the SMTP receiver listens. Port 25 must reach it.
SENDSCORE_MAIL_DOMAIN localhost The domain in test addresses, the one your MX record points here
SENDSCORE_BASE_URL http:// and the web address The public URL, used in wait_url, page_url, report_url, sign-in links, and the OAuth metadata
SENDSCORE_DB sendscore.db The SQLite database file
SENDSCORE_TLS_CERT none A certificate file for the mail domain, for STARTTLS
SENDSCORE_TLS_KEY none The certificate’s private key file
SENDSCORE_CHECK_LINKS on 0 turns off fetching links and images, and content.links and content.images are then skipped
SENDSCORE_SPAMHAUS_DQS_KEY none A Spamhaus DQS key, to look link domains up on Spamhaus DBL
SENDSCORE_SURBL_ZONE none A SURBL data feed zone, for subscribers
SENDSCORE_PUBLIC_BLOCKLISTS off 1 uses the free public Spamhaus and SURBL zones, for non-commercial use only
SENDSCORE_SECURE_COOKIES true when SENDSCORE_BASE_URL is https, otherwise false When true, cookies are marked Secure, responses carry HSTS, and the app refuses to start without SENDSCORE_BASE_URL, SENDSCORE_MAIL_DOMAIN, and the SES keys.
SES_ACCESS_KEY_ID none An Amazon SES access key, for emailing sign-in links
SES_SECRET_ACCESS_KEY none The SES key’s secret
SES_REGION us-east-1 The SES region
SENDSCORE_MAIL_FROM none, required with the SES keys The sender of sign-in emails, an address your SES account may send from
SENDSCORE_DAILY_TESTS 500 Test addresses a signed-in account may make per UTC day
SENDSCORE_ANON_DAILY_TESTS 5, or 0 on a public instance with closed sign-ups Test addresses and uploads per UTC day for each IP that is not signed in
SENDSCORE_ANON_DAILY_TOTAL 100 Test addresses and uploads per UTC day for all anonymous visitors together
SENDSCORE_ANON_DAILY_MESSAGES 200 Messages per UTC day received by all anonymous addresses together
SENDSCORE_DAILY_MESSAGES 1000 Messages an account’s test addresses may receive together per UTC day. Past it, the receiver answers 452.
SENDSCORE_MESSAGES_PER_ADDRESS 20 Messages one test address accepts in its life. Past it, the receiver answers 550.
SENDSCORE_SMTP_SESSIONS 200 SMTP connections open at once. Past it, the receiver answers 421. A connection has 2 minutes to name a real address.
SENDSCORE_SMTP_SESSIONS_PER_IP 5 SMTP connections open at once from one sending IP or IPv6 /64, and four times that from one IPv4 /24 or IPv6 /48. Past it, the receiver answers 421.
SENDSCORE_SMTP_BUFFER_MB 256 Megabytes of memory for messages being read and checked, by SMTP and uploads together. A message counts for ten times its size, about what it costs at its peak. Past it, SMTP answers 451 and an upload gets 503. It must be at least 250, or the app won’t start.
SENDSCORE_SMTP_ANALYZING 8 Messages checked at once. More wait up to 30 seconds, then get a 451.
SENDSCORE_DAILY_MB 1024 Megabytes one account may store per UTC day, across test addresses, monitoring addresses, and uploads, and the same for anonymous senders together
SENDSCORE_ACCOUNT_STORAGE_MB 2048 Megabytes of originals one account may keep at once, and the same for anonymous senders together. Past it, that sender’s mail gets a 452 and its uploads a 429, and everyone else is unaffected.
SENDSCORE_MAX_STORAGE_MB 10240 Database size at which new mail gets a 452 and uploads a 503. It counts the database alone, not the WAL, backups, or temporary files, so set it well below your free disk space.
SENDSCORE_SIGNIN_EMAILS_PER_HOUR 200 Sign-in emails per hour for the whole service, on top of 5 an hour per network and per address
SENDSCORE_OAUTH_REGISTRATIONS_PER_HOUR 600 MCP connector registrations per hour for the whole service, on top of 20 an hour per network
SENDSCORE_SIGNUPS closed Who may make an account. See Accounts and sign-in.
SENDSCORE_ALLOWED_EMAILS none Addresses that may sign up while sign-ups are closed, as a comma-separated list. Add your own before you first sign in.
SENDSCORE_PUBLICATIONS 5 Newsletters an account may monitor
SENDSCORE_HISTORY_DAYS 365 Days a monitored newsletter’s issues are kept
SENDSCORE_RAW_DAYS 30 Days the original of a message is kept. Its report stays until the message expires.
SENDSCORE_PUBLICATION_DAILY_MESSAGES 50 Messages one monitoring address accepts per UTC day. Past it, the receiver answers 452.
SENDSCORE_ALERTS_PER_DAY 10 Alerts one newsletter sends per UTC day
SENDSCORE_ALERTS_PER_ACCOUNT 30 Alerts one account sends per UTC day, however many newsletters it makes and deletes
SENDSCORE_ALERTS_PER_RECIPIENT 20 Alerts one address receives per UTC day, from all accounts
SENDSCORE_ALERTS_PER_HOUR 200 Alerts per hour for the whole service
SENDSCORE_ALERT_FROM none, required with the SES keys The sender of alert emails and alert confirmations, an address your SES account may send from
SENDSCORE_PRIVACY_URL none Your privacy policy, linked from the app’s footer and sign-in page
SENDSCORE_TERMS_URL none Your terms, linked the same way. The sign-in page mentions both only when both are set.
SENDSCORE_TRUST_PROXY off true behind a reverse proxy, so limits count the client IP from X-Forwarded-For, read only on connections from SENDSCORE_TRUSTED_PROXIES
SENDSCORE_TRUSTED_PROXIES loopback and the private networks Addresses or CIDR ranges the proxy connects from, comma separated

Set SENDSCORE_TLS_CERT and SENDSCORE_TLS_KEY to a certificate and key for the mail domain, for example from Let’s Encrypt. Without them STARTTLS is off, and every message fails auth.tls. The app checks the files once a minute and loads them again when they change, so a renewed certificate needs no restart.

One SMTP connection delivers at most 10 messages. The next MAIL gets a 451, so the sender reconnects for the rest.

Link checking fetches every link and image in each message the app receives or is given as an upload. It never fetches unsubscribe, confirm, verify, or preference links, and it refuses private, loopback, and other non-public addresses, so a message can’t make the server reach your own network.

Accounts and sign-in

An account can be deleted from its Account page, which deletes its reports, messages, newsletters, tokens, and connected apps. Accounts sign in with an emailed link, with no passwords. A link works once and expires after 15 minutes. With the SES keys set, the app emails it from SENDSCORE_MAIL_FROM, and it exits at startup unless SENDSCORE_MAIL_FROM and SENDSCORE_ALERT_FROM are both set. Without them, it writes the link to its log instead, which is fine on your own machine and nowhere else. In production, give an https SENDSCORE_BASE_URL, which turns on SENDSCORE_SECURE_COOKIES, so the app will not start until the SES keys and SENDSCORE_MAIL_DOMAIN are set, and sign-in links never come from the request’s Host header or land in the log.

Signed-in users create API tokens on the Account page, and every /api/v1/ route requires one. The MCP connector at /api/mcp takes an API token or signs a client in with OAuth, as in MCP setup. Each account sees only its own test addresses and reports.

Sign-ups are closed by default through SENDSCORE_SIGNUPS, so a stranger who finds the server can’t make an account. Put your own address in SENDSCORE_ALLOWED_EMAILS before you first sign in. Anyone else sees the same page and gets no email, so the server can’t be made to send mail to strangers. The account limits are too generous for strangers, so think before you open sign-ups on a public server. On a public instance with closed sign-ups, anonymous test addresses are off.

Set SENDSCORE_TRUST_PROXY=true only when the app sits behind a reverse proxy that sets X-Forwarded-For itself. The app reads that header only on connections from SENDSCORE_TRUSTED_PROXIES, which defaults to loopback and the private networks. The app’s README has the proxy settings.

Monitoring

Monitoring is on for every account, up to SENDSCORE_PUBLICATIONS newsletters each. Alerts are sent through SES with the same keys as sign-in emails, and without them each alert is written to the log.

The heartbeat runs inside the app, once a minute, so a missed issue is noticed only while the app is running. After a restart it catches up on deadlines from the last day.

Behind a web server

A typical setup keeps both listeners on localhost, puts a web server with TLS in front of SENDSCORE_ADDR, and forwards port 25 to SENDSCORE_SMTP_ADDR.

On shutdown, open waits answer not_received at once, uploads and SMTP sessions get up to 70 seconds to finish, and background work gets up to 15 more. Give the process manager a stop timeout of 90 seconds, which is systemd’s default.