mailcheap README

The project's README from github.com/marclou/mailcheap. As one markdown file: README.md.

Mailcheap

Mailcheap is a self-hosted newsletter app on Amazon SES. Write an issue, send it to your list, publish it on the web. About $0.10 per 1,000 emails.

Mailcheap overview

Deploy with Vercel

[!IMPORTANT] No support. This is a one-time open-source release. Issues are off, and setup questions go unanswered. Fork it, deploy it, make it yours.

Why

Newsletter platforms charge for every subscriber, every month. With your own SES account you pay for the emails you send: 10,000 readers and 4 issues a month is 40,000 emails, about $4.

Mailcheap is the rest of what a newsletter needs, in one small Next.js app: an editor, a list, signup forms, unsubscribe handling, bounce and complaint handling, stats and a public archive. One admin, no teams, no plans. Your list stays in your database.

Features

Write and send

Grow the list

Keep it healthy

Publish and measure

Screenshots

Editor Campaign report
Editor Campaign report
Contacts Public archive
Contacts Public archive
Issue page

Issue page

All names and addresses in the screenshots are made up.

What you need

Service For Cost
MongoDB Contacts, campaigns, stats Free tier works
Amazon SES Sending About $0.10 per 1,000 emails
Upstash QStash Background sending and scheduling Free tier to start, then pay per message. One message sends 50 emails.
Vercel or any Node.js host Hosting Free tier works
Upstash Redis (optional) Rate limits shared by every server instance Free tier
Vercel Blob (optional) Image uploads in the editor Free tier

Without Redis, Mailcheap counts the same rate limits in each server instance's memory. That still stops a script hammering a form, but on serverless hosts the limits aren't shared between instances and reset on each deploy. Add Redis before you put a signup form on a busy site.

Without Blob, image uploads in the editor are off. HTML and Markdown campaigns can still use images hosted anywhere.

Setup

Plan for about an hour, most of it waiting for DNS and for AWS to approve production access.

1. Database

  1. Create a free cluster on MongoDB Atlas.
  2. Database Access: add a user with a password and the "Read and write to any database" role.
  3. Network Access: allow 0.0.0.0/0. Vercel doesn't have fixed IP addresses.
  4. Connect > Drivers: copy the connection string and add a database name before the ?, like ...mongodb.net/mailcheap?retryWrites=true. That's your MONGODB_URI.

2. Deploy

  1. Click Deploy above.
  2. Fill in MONGODB_URI, ADMIN_EMAIL, ADMIN_PASSWORD (at least 12 characters) and SESSION_SECRET (at least 32; generate it with openssl rand -base64 48). With shorter ones, Mailcheap refuses to start and the logs say which one to fix.
  3. Open https://<your-project>.vercel.app/login and log in.

Sending is off until you finish the next steps, so nothing can go out by mistake. On any other Node.js host: npm ci && npm run build && npm start, with the same variables, behind HTTPS.

If you plan to use your own domain for the dashboard (like mailcheap.example.com), add it in Vercel now and set APP_URL to it. Links in your emails use APP_URL, so it should be final before your first send.

3. Amazon SES

Use one AWS region for everything below (the examples use us-east-1).

  1. Verify a sending domain. In the SES console, go to Identities > Create identity > Domain. Use a subdomain like mail.example.com, so your main domain's email is unaffected. Keep Easy DKIM with RSA 2048 and add the 3 CNAME records SES gives you at your DNS provider.
  2. Set a custom MAIL FROM domain. On the identity, open Custom MAIL FROM domain > Edit, use bounce.mail.example.com, and add the MX and TXT records SES shows. This aligns SPF with your domain.
  3. Add DMARC if your root domain doesn't have one: a TXT record at _dmarc.example.com with v=DMARC1; p=none; to start. Gmail and Yahoo require DMARC from bulk senders.
  4. Request production access. New accounts are in the sandbox (200 emails a day, verified recipients only). In Account dashboard > Request production access, pick Marketing, and explain that readers opt in, that bounces and complaints are suppressed automatically, and that every email has one-click unsubscribe. AWS usually answers within a day.
  5. Create a configuration set, named mailcheap for example, under Configuration sets. Leave SES open and click tracking off: Mailcheap tracks on your own domain.
  6. Create an SNS topic. In the SNS console, create a standard topic, like mailcheap-ses-events, and copy its ARN. Then switch it to signature version 2, which signs messages with SHA256 instead of SHA1: aws sns set-topic-attributes --topic-arn <topic-arn> --attribute-name SignatureVersion --attribute-value 2.
  7. Send SES events to the topic. In the configuration set, open Event destinations > Add destination. Select hard bounces, complaints, deliveries, delivery delays, rejects and rendering failures, then pick Amazon SNS and your topic.
  8. Create an IAM user for Mailcheap with this policy, then create an access key for it:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["ses:SendEmail", "ses:SendRawEmail"],
      "Resource": [
        "arn:aws:ses:us-east-1:<account-id>:identity/mail.example.com",
        "arn:aws:ses:us-east-1:<account-id>:configuration-set/mailcheap"
      ]
    },
    {
      "Effect": "Allow",
      "Action": ["ses:GetAccount", "ses:ListEmailIdentities", "ses:GetEmailIdentity"],
      "Resource": "*"
    }
  ]
}
  1. Add the variables in Vercel and redeploy: SES_REGION, SES_ACCESS_KEY_ID, SES_SECRET_ACCESS_KEY, SES_CONFIGURATION_SET and SES_SNS_TOPIC_ARN.
  2. Subscribe Mailcheap to the topic. In the SNS topic, Create subscription with protocol HTTPS and endpoint https://<your-app>/api/webhooks/ses. Leave raw message delivery off. Mailcheap confirms the subscription by itself; refresh and the status turns to Confirmed.

The Domains page in Mailcheap now shows your domain with its DKIM and MAIL FROM status. Optional: in SES Account dashboard > Suppression list, turn on the account-level list for bounces and complaints too.

4. QStash

  1. In the Upstash console, open QStash and copy QSTASH_TOKEN, QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY (and QSTASH_URL if you picked a region other than the default).
  2. Add them in Vercel and redeploy.

QStash calls APP_URL/api/jobs/... to send each batch, so the app has to be reachable on the internet. Mailcheap checks QStash's signature on every job.

5. Turn sending on

  1. In Mailcheap, create a newsletter. The from address must be on your verified domain, and the postal address shows in every email's footer (anti-spam laws require one).
  2. Set SENDING_ENABLED=true in Vercel and redeploy.
  3. Write a campaign, send yourself a test, then send. For a new domain, set a limit to reach your most engaged readers first, and use Send to more on the report once the first step looks healthy.

Optional

Local development

git clone https://github.com/<you>/mailcheap.git && cd mailcheap
npm install
cp .env.example .env.local   # set MONGODB_URI, ADMIN_EMAIL, ADMIN_PASSWORD, SESSION_SECRET
npm run dev

A local MongoDB works: docker run -d -p 27017:27017 mongo and MONGODB_URI=mongodb://127.0.0.1:27017/mailcheap.

Deliverability basics

Environment variables

Variable Required What it does
MONGODB_URI Yes MongoDB connection string with a database name
ADMIN_EMAIL Yes Dashboard login, and where test emails and alerts go
ADMIN_PASSWORD Yes Dashboard password, 12+ characters. Changing it signs out every session.
SESSION_SECRET Yes Signs sessions and email links. 32+ random characters. Changing it breaks links in sent emails.
APP_URL On other hosts Public URL of the app. On Vercel it defaults to the production domain.
SENDING_ENABLED To send Nothing is sent unless it's true
SES_REGION To send SES region, us-east-1 by default
SES_ACCESS_KEY_ID, SES_SECRET_ACCESS_KEY To send IAM user keys
SES_CONFIGURATION_SET To send Configuration set that publishes events to SNS
SES_SNS_TOPIC_ARN To send SNS topic Mailcheap accepts at /api/webhooks/ses
QSTASH_TOKEN, QSTASH_CURRENT_SIGNING_KEY, QSTASH_NEXT_SIGNING_KEY To send campaigns Background sending and scheduling
QSTASH_URL No QStash region other than the default, or a local QStash dev server
SEND_RATE_PER_SECOND No Emails per second, 1 to 14 (default 10). Use 6 on a free Atlas cluster.
ARCHIVE_HOSTS No Archive domains, like news.example.com=weekly,letters.example.org=monthly
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN No Shared rate limits
BLOB_READ_WRITE_TOKEN No Image uploads in the editor (set by Vercel Blob)
ALERT_FROM No From address for alert emails, on a verified domain

.env.example has the same list with more detail.

API

Create a key on the API keys page and send it as Authorization: Bearer <key>.

Add a contact: POST /api/v1/contacts

curl -X POST https://<your-app>/api/v1/contacts \
  -H "Authorization: Bearer $MAILCHEAP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"newsletter":"weekly","email":"[email protected]","source":"website","ip":"203.0.113.7"}'

Create a draft: POST /api/v1/campaigns with newsletter, subject, previewText (optional), html or markdown, and archiveSlug (optional). Answers 201 with the draft's id and dashboard URL. Sending stays a click in the dashboard.

List sent issues: GET /api/v1/campaigns?newsletter=<slug> returns each issue's id, subject, preview text, archive URL and send date.

How it works

Support

There's none: issues are off and pull requests may not be reviewed. This is a one-time release, with no updates planned. See SECURITY.md for what's built in.

License

MIT. Made by Marc Lou.

mailcheap (marclou/mailcheap)

Searching Database…

Что это такое?Mailcheap — самостоятельно размещаемое приложение для рассылок, которое отправляет письма через ваш собственный аккаунт Amazon SES: вы пишете выпуск, отправляете его своему списку и публикуете в вебе — примерно $0.10 за 1000 писем.
Это хостинговый сервис или то, что я запускаю сам?Это не SaaS, на который вы подписываетесь, — это небольшое приложение Next.js, которое вы разворачиваете сами (на Vercel или любом Node.js-хосте), со своей базой данных MongoDB и своим аккаунтом Amazon SES.
Как его запустить?Создайте бесплатный кластер MongoDB Atlas, нажмите Deploy to Vercel и заполните MONGODB_URI, ADMIN_EMAIL, ADMIN_PASSWORD и SESSION_SECRET, затем подключите Amazon SES и Upstash QStash, прежде чем включать SENDING_ENABLED. На других Node.js-хостах это npm ci && npm run build && npm start.
Какие сервисы нужны, кроме самого приложения?MongoDB для контактов и статистики, Amazon SES для отправки, Upstash QStash для фоновой отправки и планирования, а также Node.js-хост вроде Vercel; Upstash Redis и Vercel Blob опциональны — для общих лимитов частоты и загрузки изображений.
Есть ли поддержка, если что-то сломается?Нет. Проект описан как разовый open-source релиз с выключенными issues и оставленными без ответов вопросами по настройке — вы делаете форк и запускаете его сами.
Где хранятся данные моих подписчиков?В вашей собственной базе данных MongoDB — Mailcheap не прогоняет ваш список ни через одну стороннюю платформу рассылок.
Что мешает неудачной рассылке навредить репутации моего отправителя?Mailcheap автоматически приостанавливает кампанию, как только отказы превысят 2% или жалобы — 0.08%, поддерживает подтверждённое opt-in согласие и может автоматически отписывать читателей, игнорирующих несколько выпусков подряд.
Можно ли вести больше одной рассылки с одной установки?Да — у каждой рассылки свои адрес отправителя, reply-to, почтовый адрес и форма подписки, но один общий вход администратора и один список подавления.
Как он обрабатывает отписки и отказы?Он отправляет заголовки отписки в один клик RFC 8058, подписанную страницу отписки и слушает события отказов/жалоб SES через SNS, поддерживая общий список подавления в актуальном состоянии.
Можно ли изолировать публичный архив от админ-панели?Да, установка ARCHIVE_HOSTS выносит страницы выпусков, страницу автора и ссылки отписки на отдельный домен, так что недоверенный HTML из редактора никогда не разделяет origin с панелью управления.
Creator
marclou
Size
2.5 MB
Stars
1.2K
Updated
CommitOct 16
Age
7d
Commits · 90 Days