auto mail generation
This commit is contained in:
168
docs/MAIL_SETUP.md
Normal file
168
docs/MAIL_SETUP.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Mail setup — Postal, sending as care@nearledaily.com
|
||||
|
||||
What this is for: the first-password invitation. Every back-office account on
|
||||
Fiesta is created with an empty password, and the link in this email is the only
|
||||
way to set one — the sign-in screen no longer offers a form, because a public one
|
||||
meant that knowing a merchant's email address was enough to claim their account.
|
||||
|
||||
So this is not newsletter plumbing. **If the mail lands in spam, a business that
|
||||
was just onboarded cannot sign in**, and the first anyone hears of it is a phone
|
||||
call. The DNS section below matters more than the install.
|
||||
|
||||
---
|
||||
|
||||
## The decisions already made
|
||||
|
||||
| | | why |
|
||||
|---|---|---|
|
||||
| Server | Postal, self-hosted | open source, purpose-built for transactional mail, speaks plain SMTP so nothing in Go changes |
|
||||
| Sender | `care@nearledaily.com` | the link points at `app.nearledaily.com`; a password mail whose sender and destination are different domains is the shape of a phishing mail |
|
||||
| `care@` not `no-reply@` | | someone replying "I never got this" is the most useful reply this system can get, and it should reach a person |
|
||||
| Outbound | relay through a smarthost at first | see [Delivery](#delivery-the-hard-half) |
|
||||
|
||||
---
|
||||
|
||||
## 1. Stand Postal up
|
||||
|
||||
Postal needs a host of its own — it wants ports 25, 80 and 443, plus MariaDB and
|
||||
RabbitMQ. A 2 vCPU / 4 GB box is ample for our volume.
|
||||
|
||||
```sh
|
||||
# on the mail host
|
||||
git clone https://github.com/postalserver/install /opt/postal/install
|
||||
ln -s /opt/postal/install/bin/postal /usr/bin/postal
|
||||
postal bootstrap postal.nearledaily.com
|
||||
postal initialize
|
||||
postal make-user # your admin login
|
||||
postal start
|
||||
```
|
||||
|
||||
Then in Postal's web UI:
|
||||
|
||||
1. **Create an organisation** — `Nearle`.
|
||||
2. **Add a mail server** under it — call it `transactional`. Keep marketing mail
|
||||
out of this one forever; shared reputation is the whole point.
|
||||
3. **Add the domain** `nearledaily.com`. Postal prints the DNS records it wants.
|
||||
Section 2 is those records.
|
||||
4. **Create a credential** of type *SMTP*, scoped to that server. Postal gives
|
||||
you a username and password pair. **This is not a mailbox login** — it exists
|
||||
only for Fiesta to authenticate with, and it can be revoked on its own.
|
||||
|
||||
---
|
||||
|
||||
## 2. DNS on nearledaily.com
|
||||
|
||||
This is the part that decides whether the invitation is read or binned. Postal's
|
||||
domain page shows the exact values; the shapes are:
|
||||
|
||||
| Record | Name | Value |
|
||||
|---|---|---|
|
||||
| TXT (SPF) | `nearledaily.com` | `v=spf1 a mx include:spf.postal.nearledaily.com ~all` |
|
||||
| TXT (DKIM) | `postal._domainkey.nearledaily.com` | the public key Postal generates |
|
||||
| CNAME (Return-Path) | `psrp.nearledaily.com` | `rp.postal.nearledaily.com` |
|
||||
| TXT (DMARC) | `_dmarc.nearledaily.com` | `v=DMARC1; p=none; rua=mailto:care@nearledaily.com` |
|
||||
| PTR (rDNS) | the mail host's IP | `postal.nearledaily.com` — set at your VPS provider, not in DNS |
|
||||
|
||||
Notes that save an afternoon:
|
||||
|
||||
- **One SPF record per domain.** If `nearledaily.com` already has one, merge the
|
||||
`include:` into it rather than adding a second — two SPF records is a permerror
|
||||
and fails every check.
|
||||
- **Start DMARC at `p=none`.** It reports without rejecting. Read the reports for
|
||||
a couple of weeks, confirm everything legitimate is aligned, then move to
|
||||
`p=quarantine`. Going straight to `p=reject` is how you discover a misaligned
|
||||
sender by losing its mail.
|
||||
- **The Return-Path CNAME is not optional.** Without it, bounces go nowhere and
|
||||
Postal cannot tell you a merchant's address is dead — which, for this mail, is
|
||||
exactly the fact you most need.
|
||||
- Verify with `dig TXT nearledaily.com`, and send a test to a
|
||||
[mail-tester.com](https://mail-tester.com) address. Aim for 9/10 or better
|
||||
before the first real merchant.
|
||||
|
||||
---
|
||||
|
||||
## 3. Point Fiesta at it
|
||||
|
||||
Credentials go in `.env.secrets`, which is read first and is the only env file
|
||||
git ignores. Never in `.env` — that one is tracked and shared.
|
||||
|
||||
```sh
|
||||
# .env.secrets on the Fiesta host
|
||||
MAIL_HOST=postal.nearledaily.com
|
||||
MAIL_USERNAME=<from Postal's SMTP credential>
|
||||
MAIL_PASSWORD=<from Postal's SMTP credential>
|
||||
```
|
||||
|
||||
Everything else is already set in `.env`:
|
||||
|
||||
```sh
|
||||
MAIL_PORT=587
|
||||
MAIL_FROM=care@nearledaily.com
|
||||
MAIL_FROM_NAME=Nearle
|
||||
MAIL_CONSOLE_URL=https://app.nearledaily.com
|
||||
```
|
||||
|
||||
`MAIL_CONSOLE_URL` is the **merchant** console and never the platform one — a
|
||||
merchant sets their password at `app.nearledaily.com/set-password` and nowhere
|
||||
else.
|
||||
|
||||
Restart and read the first log line:
|
||||
|
||||
```
|
||||
mail: sending as care@nearledaily.com via postal.nearledaily.com:587
|
||||
```
|
||||
|
||||
If it instead says `mail: OFF — <reason>`, the reason names the missing variable.
|
||||
Nothing else breaks: the server boots, onboarding works, and every create answers
|
||||
`invited: false` with that same reason on screen.
|
||||
|
||||
---
|
||||
|
||||
## 4. Prove it end to end
|
||||
|
||||
Not "the config looks right" — actually watch one arrive.
|
||||
|
||||
1. Onboard a test merchant in the platform console with an address you can read.
|
||||
2. The success screen should say the invitation is on its way. If it says
|
||||
**No invitation was sent**, the reason on screen is the server's own.
|
||||
3. Open the mail. Check it is **not** in spam — that is the whole test.
|
||||
4. Follow the link, set a password, sign in at `app.nearledaily.com`.
|
||||
5. Press **Resend invite** on that tenant. It must refuse, naming the business:
|
||||
*"… has already set a password — send them to the sign-in page instead."*
|
||||
That refusal is what stops this becoming a password reset.
|
||||
|
||||
---
|
||||
|
||||
## Delivery, the hard half
|
||||
|
||||
Postal is the easy part. Getting mail *accepted* from your own IP is not:
|
||||
|
||||
- Most clouds block outbound port 25 by default. AWS, GCP, Azure, DigitalOcean,
|
||||
Oracle and Hetzner all require an exception request; some decline.
|
||||
- A fresh IP has no sending reputation. Gmail and Outlook throttle or spam-folder
|
||||
it until it is warmed over weeks. A recycled VPS IP is frequently already on
|
||||
Spamhaus — check before you commit to one.
|
||||
- rDNS must match the HELO hostname. Not every provider lets you set it.
|
||||
|
||||
**So configure Postal to relay outbound through a smarthost to begin with.** You
|
||||
keep the open-source stack, your own queue, your own logs and the freedom to
|
||||
move — and you borrow established IP reputation for the last hop only. Postal
|
||||
supports this per mail server under *Settings → SMTP relays*. Once you have
|
||||
volume and a warm dedicated IP, cut over to sending directly; nothing on the
|
||||
Fiesta side changes, because it only ever talks to Postal.
|
||||
|
||||
At our volume — a few dozen invitations a month — carrying full deliverability
|
||||
operations to save roughly ₹1,000 a year is a bad trade against one merchant
|
||||
locked out of their own business.
|
||||
|
||||
---
|
||||
|
||||
## What the merchant actually receives
|
||||
|
||||
Plain text, deliberately. A password link arriving as an image-heavy HTML
|
||||
template is the shape of a phishing mail, and plain text renders identically
|
||||
everywhere. The body names the business, puts the link on its own line, and says
|
||||
it expires in seven days — because an invitation found three weeks later needs to
|
||||
explain itself rather than look broken.
|
||||
|
||||
The wording lives in `inviteMessage` in `services/inviteService.go`.
|
||||
Reference in New Issue
Block a user