nutrition docker file fix

This commit is contained in:
2026-09-30 14:58:30 +05:30
parent c49f5372a5
commit d0804ae84f
8 changed files with 430 additions and 112 deletions

View File

@@ -1,4 +1,4 @@
# Mail setup — Postal, sending as care@nearledaily.com
# Mail setup — Google Workspace SMTP, 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
@@ -7,93 +7,87 @@ 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.
call. Step 3 is the one that decides that, and it is the one people skip.
---
## The decisions already made
## The decisions
| | | why |
|---|---|---|
| Server | Postal, self-hosted | open source, purpose-built for transactional mail, speaks plain SMTP so nothing in Go changes |
| Relay | Google Workspace SMTP | no server to run, no IP to warm, no port 25 exception to beg for. At a few dozen invitations a month that is the whole argument |
| 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) |
| `care@` not `no-reply@` | | somebody replying "I never got this" is the most useful reply this system can receive, and it should reach a person |
| Auth | an App Password, never the login password | it can be revoked on its own if it leaks |
This replaces an earlier plan to self-host Postal. Postal is the better answer at
volume; it is the wrong answer for tens of emails a month, because the work is
not the software — it is IP reputation, rDNS and blocklists.
---
## 1. Stand Postal up
## Step 1 — Google Workspace on nearledaily.com
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.
1. Sign up at workspace.google.com with `nearledaily.com`. Business Starter is
enough.
2. Verify the domain with the TXT record Google gives you.
3. Create `care@nearledaily.com`. It is a real mailbox and **somebody has to read
it** — merchant replies and bounce notices both land there, and a bounce is how
you learn an invitation never arrived.
```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:
## Step 2 — DNS on nearledaily.com
| 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 |
| MX | `nearledaily.com` | `smtp.google.com` (priority 1) |
| TXT (SPF) | `nearledaily.com` | `v=spf1 include:_spf.google.com ~all` |
| TXT (DKIM) | `google._domainkey` | the key from Step 3 |
| TXT (DMARC) | `_dmarc` | `v=DMARC1; p=none; rua=mailto:care@nearledaily.com` |
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
- **One SPF record only.** If the domain already has one, merge
`include:_spf.google.com` into it. Two SPF records is a permerror and fails
every check.
- **Remove old MX records** if the domain receives mail somewhere else today, or
that mail keeps going to the old place.
- **Keep DMARC at `p=none`** for a couple of weeks, read the reports, then move to
`p=quarantine`. Going straight to `p=reject` is how you find 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.
---
## Step 3 — DKIM (the step people skip)
## 3. Point Fiesta at it
Admin console → Apps → Google Workspace → Gmail → **Authenticate email**.
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.
1. **Generate new record** (2048-bit), add the TXT record it prints to DNS.
2. Wait for DNS to propagate — minutes to hours.
3. Come back and click **Start authentication**.
Until you click that last button the mail is unsigned, and unsigned mail carrying
a password link goes to spam.
## Step 4 — An App Password for Fiesta
1. Sign in as `care@nearledaily.com` → Google Account → Security → turn on
**2-Step Verification**.
2. Security → **App passwords** → create one named `Fiesta`. You get 16
characters.
3. That is what Fiesta uses. Never the account's real password.
No App passwords option? An admin has to allow it, or set up Admin console →
Gmail → Routing → **SMTP relay service** with "require SMTP authentication" and
"require TLS". In that case `MAIL_HOST` becomes `smtp-relay.gmail.com`.
## Step 5 — 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>
MAIL_HOST=smtp.gmail.com
MAIL_USERNAME=care@nearledaily.com
MAIL_PASSWORD=<16-character app password>
```
Everything else is already set in `.env`:
Already set in `.env`:
```sh
MAIL_PORT=587
@@ -102,62 +96,76 @@ MAIL_FROM_NAME=Nearle
MAIL_CONSOLE_URL=https://app.nearledaily.com
```
Google shows the App Password as four groups — `abcd efgh ijkl mnop`. Paste it
with or without the spaces; Fiesta strips them for Google SMTP hosts only, and
only when what remains is the sixteen alphanumerics an App Password actually is.
Another relay's password is never edited.
`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:
Restart. The log says which state it is in:
```
mail: sending as care@nearledaily.com via postal.nearledaily.com:587
mail: sending as care@nearledaily.com via smtp.gmail.com:587
mail: OFF — <reason naming the missing variable>
```
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.
**On a hosted deployment these belong in the platform's own environment**
(Dokploy), not in a file in the repo. A `.env` committed to the repository is
overwritten at build time — that is how the nutrition service shipped switched
off.
---
### What Fiesta does with them
## 4. Prove it end to end
`utils/mail.go` upgrades to TLS with STARTTLS before authenticating, and
**refuses to send at all if a relay offers no encryption while credentials are
configured**. Go's own `smtp.PlainAuth` would decline to hand over the password
anyway, so nothing leaks either way — but it reports that as the server refusing
the credentials, which sends somebody to check the password when the problem is
the connection. It also closes a downgrade, where an attacker strips STARTTLS
from the greeting.
Not "the config looks right" — actually watch one arrive.
## Step 6 — Check the DNS
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.
```sh
dig TXT nearledaily.com +short # SPF, with _spf.google.com
dig TXT google._domainkey.nearledaily.com +short # DKIM key
dig TXT _dmarc.nearledaily.com +short # DMARC
dig MX nearledaily.com +short # smtp.google.com
```
Google's Check MX tool at toolbox.googleapps.com does the same job.
## Step 7 — Prove it end to end
Not "the config looks right" — watch one arrive.
1. Send a test to a [mail-tester.com](https://mail-tester.com) address. Aim for
9/10 or better **before** a real merchant sees one.
2. Onboard a test merchant with an address you can read.
3. Confirm it is in the **inbox, not spam**. In Gmail, "Show original" should
show SPF, DKIM and DMARC all PASS.
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:
5. Press **Resend invite**. 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
## Worth knowing
Postal is the easy part. Getting mail *accepted* from your own IP is not:
- **Limit:** about 2,000 messages a day per user. Onboarding runs at a few dozen
a month, so this is not a constraint.
- **Bounces** arrive as "Delivery failed" in the `care@` inbox. Nothing in Fiesta
watches for them, so somebody has to read that mailbox after onboarding.
- **Not for bulk.** Google does not permit marketing sends through Workspace. If
newsletters are ever wanted, that is a separate provider — not this mailbox.
- **If the App Password leaks:** revoke it in Google Account → Security, issue a
new one, update `.env.secrets`. Nothing else has to change.
- 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
## What the merchant 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
@@ -165,4 +173,4 @@ 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`.
The wording is `inviteMessage` in `services/inviteService.go`.