@benclmnt/postmock 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +251 -0
- package/dist/seeds/conformance/00-core.d.ts +4 -0
- package/dist/seeds/conformance/00-core.js +12 -0
- package/dist/seeds/conformance/20-bounces.d.ts +3 -0
- package/dist/seeds/conformance/20-bounces.js +108 -0
- package/dist/seeds/conformance/40-messages.d.ts +3 -0
- package/dist/seeds/conformance/40-messages.js +144 -0
- package/dist/seeds/conformance/50-server-color.d.ts +8 -0
- package/dist/seeds/conformance/50-server-color.js +13 -0
- package/dist/seeds/conformance/70-account.d.ts +9 -0
- package/dist/seeds/conformance/70-account.js +29 -0
- package/dist/seeds/conformance/80-templates.d.ts +4 -0
- package/dist/seeds/conformance/80-templates.js +23 -0
- package/dist/seeds/conformance.d.ts +2 -0
- package/dist/seeds/conformance.js +3 -0
- package/dist/seeds/empty.d.ts +4 -0
- package/dist/seeds/empty.js +3 -0
- package/dist/seeds/lib/conformance.d.ts +16 -0
- package/dist/seeds/lib/conformance.js +18 -0
- package/dist/seeds/lib/history.d.ts +27 -0
- package/dist/seeds/lib/history.js +129 -0
- package/dist/seeds/lib/read-server.d.ts +10 -0
- package/dist/seeds/lib/read-server.js +10 -0
- package/dist/seeds/lib/template-server.d.ts +13 -0
- package/dist/seeds/lib/template-server.js +13 -0
- package/dist/src/api/account/authentication.d.ts +39 -0
- package/dist/src/api/account/authentication.js +97 -0
- package/dist/src/api/account/domains.d.ts +59 -0
- package/dist/src/api/account/domains.js +78 -0
- package/dist/src/api/account/paging.d.ts +12 -0
- package/dist/src/api/account/paging.js +35 -0
- package/dist/src/api/account/push.d.ts +14 -0
- package/dist/src/api/account/push.js +84 -0
- package/dist/src/api/account/routes.d.ts +1 -0
- package/dist/src/api/account/routes.js +288 -0
- package/dist/src/api/account/senders.d.ts +54 -0
- package/dist/src/api/account/senders.js +87 -0
- package/dist/src/api/account/servers.d.ts +40 -0
- package/dist/src/api/account/servers.js +148 -0
- package/dist/src/api/bounces/json.d.ts +25 -0
- package/dist/src/api/bounces/json.js +27 -0
- package/dist/src/api/bounces/routes.d.ts +1 -0
- package/dist/src/api/bounces/routes.js +127 -0
- package/dist/src/api/bulk/bulk.d.ts +43 -0
- package/dist/src/api/bulk/bulk.js +81 -0
- package/dist/src/api/bulk/routes.d.ts +1 -0
- package/dist/src/api/bulk/routes.js +252 -0
- package/dist/src/api/data-removals/routes.d.ts +1 -0
- package/dist/src/api/data-removals/routes.js +59 -0
- package/dist/src/api/email/json.d.ts +11 -0
- package/dist/src/api/email/json.js +33 -0
- package/dist/src/api/email/routes.d.ts +1 -0
- package/dist/src/api/email/routes.js +64 -0
- package/dist/src/api/inbound/routes.d.ts +1 -0
- package/dist/src/api/inbound/routes.js +40 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +3 -0
- package/dist/src/api/message-streams/routes.d.ts +1 -0
- package/dist/src/api/message-streams/routes.js +211 -0
- package/dist/src/api/message-streams/streams.d.ts +28 -0
- package/dist/src/api/message-streams/streams.js +50 -0
- package/dist/src/api/messages/json.d.ts +149 -0
- package/dist/src/api/messages/json.js +114 -0
- package/dist/src/api/messages/query.d.ts +19 -0
- package/dist/src/api/messages/query.js +64 -0
- package/dist/src/api/messages/routes.d.ts +1 -0
- package/dist/src/api/messages/routes.js +203 -0
- package/dist/src/api/server/json.d.ts +26 -0
- package/dist/src/api/server/json.js +25 -0
- package/dist/src/api/server/routes.d.ts +1 -0
- package/dist/src/api/server/routes.js +17 -0
- package/dist/src/api/stats/aggregate.d.ts +58 -0
- package/dist/src/api/stats/aggregate.js +66 -0
- package/dist/src/api/stats/routes.d.ts +1 -0
- package/dist/src/api/stats/routes.js +72 -0
- package/dist/src/api/suppressions/routes.d.ts +1 -0
- package/dist/src/api/suppressions/routes.js +98 -0
- package/dist/src/api/templates/content.d.ts +21 -0
- package/dist/src/api/templates/content.js +39 -0
- package/dist/src/api/templates/routes.d.ts +1 -0
- package/dist/src/api/templates/routes.js +164 -0
- package/dist/src/api/templates/send.d.ts +13 -0
- package/dist/src/api/templates/send.js +105 -0
- package/dist/src/api/templates/templates.d.ts +100 -0
- package/dist/src/api/templates/templates.js +218 -0
- package/dist/src/api/templates/validate.d.ts +15 -0
- package/dist/src/api/templates/validate.js +77 -0
- package/dist/src/api/triggers/routes.d.ts +1 -0
- package/dist/src/api/triggers/routes.js +62 -0
- package/dist/src/api/webhooks/routes.d.ts +14 -0
- package/dist/src/api/webhooks/routes.js +192 -0
- package/dist/src/config.d.ts +17 -0
- package/dist/src/config.js +95 -0
- package/dist/src/control/app.d.ts +5 -0
- package/dist/src/control/app.js +42 -0
- package/dist/src/control/endpoints/account.d.ts +1 -0
- package/dist/src/control/endpoints/account.js +68 -0
- package/dist/src/control/endpoints/bulk.d.ts +1 -0
- package/dist/src/control/endpoints/bulk.js +29 -0
- package/dist/src/control/endpoints/core.d.ts +1 -0
- package/dist/src/control/endpoints/core.js +104 -0
- package/dist/src/control/endpoints/inbound.d.ts +1 -0
- package/dist/src/control/endpoints/inbound.js +61 -0
- package/dist/src/control/endpoints/recipients.d.ts +1 -0
- package/dist/src/control/endpoints/recipients.js +136 -0
- package/dist/src/control/endpoints/smtp.d.ts +1 -0
- package/dist/src/control/endpoints/smtp.js +74 -0
- package/dist/src/control/endpoints/tracking.d.ts +1 -0
- package/dist/src/control/endpoints/tracking.js +95 -0
- package/dist/src/control/endpoints/webhooks.d.ts +1 -0
- package/dist/src/control/endpoints/webhooks.js +36 -0
- package/dist/src/control/index.d.ts +1 -0
- package/dist/src/control/index.js +3 -0
- package/dist/src/control/registry.d.ts +25 -0
- package/dist/src/control/registry.js +19 -0
- package/dist/src/control/seed.d.ts +11 -0
- package/dist/src/control/seed.js +32 -0
- package/dist/src/control/seeding.d.ts +10 -0
- package/dist/src/control/seeding.js +27 -0
- package/dist/src/discover.d.ts +16 -0
- package/dist/src/discover.js +49 -0
- package/dist/src/errors.d.ts +49 -0
- package/dist/src/errors.js +701 -0
- package/dist/src/events.d.ts +49 -0
- package/dist/src/events.js +19 -0
- package/dist/src/http/app.d.ts +8 -0
- package/dist/src/http/app.js +51 -0
- package/dist/src/http/auth.d.ts +7 -0
- package/dist/src/http/auth.js +32 -0
- package/dist/src/http/faults.d.ts +8 -0
- package/dist/src/http/faults.js +33 -0
- package/dist/src/http/normalize.d.ts +57 -0
- package/dist/src/http/normalize.js +225 -0
- package/dist/src/http/respond.d.ts +23 -0
- package/dist/src/http/respond.js +37 -0
- package/dist/src/http/routes.d.ts +60 -0
- package/dist/src/http/routes.js +64 -0
- package/dist/src/inbound/deliver.d.ts +9 -0
- package/dist/src/inbound/deliver.js +34 -0
- package/dist/src/inbound/parse.d.ts +21 -0
- package/dist/src/inbound/parse.js +71 -0
- package/dist/src/inbound/receive.d.ts +17 -0
- package/dist/src/inbound/receive.js +89 -0
- package/dist/src/main.d.ts +2 -0
- package/dist/src/main.js +14 -0
- package/dist/src/mime/compose.d.ts +33 -0
- package/dist/src/mime/compose.js +58 -0
- package/dist/src/pipeline/addresses.d.ts +6 -0
- package/dist/src/pipeline/addresses.js +38 -0
- package/dist/src/pipeline/inactive.d.ts +8 -0
- package/dist/src/pipeline/inactive.js +20 -0
- package/dist/src/pipeline/submit.d.ts +138 -0
- package/dist/src/pipeline/submit.js +415 -0
- package/dist/src/plugins/message-events.d.ts +7 -0
- package/dist/src/plugins/message-events.js +76 -0
- package/dist/src/plugins/smtp.d.ts +15 -0
- package/dist/src/plugins/smtp.js +42 -0
- package/dist/src/plugins/stats.d.ts +7 -0
- package/dist/src/plugins/stats.js +78 -0
- package/dist/src/plugins/webhooks.d.ts +4 -0
- package/dist/src/plugins/webhooks.js +80 -0
- package/dist/src/plugins.d.ts +20 -0
- package/dist/src/plugins.js +9 -0
- package/dist/src/recipients/test-bounces.d.ts +15 -0
- package/dist/src/recipients/test-bounces.js +31 -0
- package/dist/src/recipients/transitions.d.ts +57 -0
- package/dist/src/recipients/transitions.js +279 -0
- package/dist/src/render/mustachio.d.ts +60 -0
- package/dist/src/render/mustachio.js +265 -0
- package/dist/src/runtime.d.ts +12 -0
- package/dist/src/runtime.js +11 -0
- package/dist/src/server.d.ts +25 -0
- package/dist/src/server.js +49 -0
- package/dist/src/smtp/auth.d.ts +35 -0
- package/dist/src/smtp/auth.js +66 -0
- package/dist/src/smtp/listener.d.ts +16 -0
- package/dist/src/smtp/listener.js +121 -0
- package/dist/src/smtp/mime.d.ts +36 -0
- package/dist/src/smtp/mime.js +181 -0
- package/dist/src/smtp/receive.d.ts +21 -0
- package/dist/src/smtp/receive.js +89 -0
- package/dist/src/smtp/reply.d.ts +14 -0
- package/dist/src/smtp/reply.js +24 -0
- package/dist/src/state/clock.d.ts +35 -0
- package/dist/src/state/clock.js +107 -0
- package/dist/src/state/ids.d.ts +6 -0
- package/dist/src/state/ids.js +7 -0
- package/dist/src/state/servers.d.ts +27 -0
- package/dist/src/state/servers.js +101 -0
- package/dist/src/state/store.d.ts +57 -0
- package/dist/src/state/store.js +98 -0
- package/dist/src/state/suppressions.d.ts +9 -0
- package/dist/src/state/suppressions.js +8 -0
- package/dist/src/state/types.d.ts +530 -0
- package/dist/src/state/types.js +41 -0
- package/dist/src/time.d.ts +11 -0
- package/dist/src/time.js +48 -0
- package/dist/src/tracking.d.ts +53 -0
- package/dist/src/tracking.js +150 -0
- package/dist/src/webhooks/deliver.d.ts +60 -0
- package/dist/src/webhooks/deliver.js +172 -0
- package/dist/src/webhooks/egress.d.ts +19 -0
- package/dist/src/webhooks/egress.js +40 -0
- package/dist/src/webhooks/outbound.d.ts +26 -0
- package/dist/src/webhooks/outbound.js +47 -0
- package/dist/src/webhooks/payloads.d.ts +111 -0
- package/dist/src/webhooks/payloads.js +128 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bennett Clement
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# postmock
|
|
2
|
+
|
|
3
|
+
A mock Postmark server in TypeScript, for testing code that sends email through Postmark.
|
|
4
|
+
Real, unmodified clients talk to it: every official Postmark SDK, and plain SMTP clients.
|
|
5
|
+
Tests then read what was sent, seed suppressions and bounces, inject faults, and trigger webhooks.
|
|
6
|
+
|
|
7
|
+
> Not affiliated with or endorsed by Postmark or ActiveCampaign. "Postmark" is their trademark.
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
|
|
11
|
+
W0 to W3 are built: REST and account APIs, SMTP, webhooks, the control API, a conformance runner for every official SDK, the npm package, the Docker image and CI.
|
|
12
|
+
[`docs/11`](docs/11-build-plan.md) is the build plan; [`ARCHITECTURE.md`](ARCHITECTURE.md) marks what is built.
|
|
13
|
+
The docs record Postmark's behavior from its public docs, its Swagger specs, and the official SDKs.
|
|
14
|
+
One behavior is CAPTURED from real Postmark so far: the sender check ([`docs/03`](docs/03-sending.md) §3.4). [`docs/10`](docs/10-live-capture-plan.md) plans the rest.
|
|
15
|
+
|
|
16
|
+
## Run
|
|
17
|
+
|
|
18
|
+
| Way | Command |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| npm package (`npm pack` builds it) | `npx @benclmnt/postmock --seed conformance` |
|
|
21
|
+
| From a checkout | `pnpm install`, then `pnpm start --seed conformance` |
|
|
22
|
+
| Docker image | `docker build -t postmock .`, then `docker run --rm -p 127.0.0.1:8080:8080 -p 127.0.0.1:8025:8025 -p 127.0.0.1:2525:2525 postmock --seed conformance` |
|
|
23
|
+
| Compose, with the Postmark host names | Option B below |
|
|
24
|
+
|
|
25
|
+
The control API and SMTP have no real authentication: publish their ports on `127.0.0.1` only.
|
|
26
|
+
|
|
27
|
+
Startup prints one `name=url` per listener:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
postmock api=http://127.0.0.1:8080 smtp=smtp://127.0.0.1:53648 control=http://127.0.0.1:8025 seed=conformance
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Every setting is an env key and a flag (`postmock --help`). A flag wins over the env:
|
|
34
|
+
|
|
35
|
+
| Flag | Env | Default | Effect |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| `--host` | `POSTMOCK_HOST` | `127.0.0.1` (image: `0.0.0.0`) | Bind address of every listener |
|
|
38
|
+
| `--api-port` | `POSTMOCK_API_PORT` | `8080` | REST over plain http |
|
|
39
|
+
| `--control-port` | `POSTMOCK_CONTROL_PORT` | `8025` | Control API |
|
|
40
|
+
| `--seed` | `POSTMOCK_SEED` | `empty` | Seed applied at start: `empty`, `conformance` ([`CONTROL-API.md`](CONTROL-API.md#seeds)) |
|
|
41
|
+
| `--https-port`, `--https-tls-key`, `--https-tls-cert` | `POSTMOCK_HTTPS_PORT`, `POSTMOCK_HTTPS_TLS_KEY`, `POSTMOCK_HTTPS_TLS_CERT` | off | REST over TLS; PEM files; all three or none |
|
|
42
|
+
| `--smtp-ports` | `POSTMOCK_SMTP_PORTS` | `0`, a free port (image: `2525`) | Comma list; every port serves the same SMTP endpoint |
|
|
43
|
+
| `--smtp-tls-key`, `--smtp-tls-cert` | `POSTMOCK_SMTP_TLS_KEY`, `POSTMOCK_SMTP_TLS_CERT` | off | PEM files; with both, SMTP offers STARTTLS |
|
|
44
|
+
| `--webhooks-allow-hosts` | `POSTMOCK_WEBHOOKS_ALLOW_HOSTS` | none | Webhook targets other than loopback; `*` for all |
|
|
45
|
+
|
|
46
|
+
Port `0` picks a free port.
|
|
47
|
+
A bad value stops startup: an unknown flag, an empty value, a port that is not a decimal number, or a PEM file that does not exist.
|
|
48
|
+
|
|
49
|
+
## Reach postmock from a client
|
|
50
|
+
|
|
51
|
+
A client reaches postmock with no code change ([`docs/01`](docs/01-client-reachability.md) §3.3).
|
|
52
|
+
Use a token that only postmock knows, such as the `conformance` seed token `postmock-server-token`.
|
|
53
|
+
A misrouted request then gets 401 from real Postmark and sends nothing.
|
|
54
|
+
|
|
55
|
+
### Option A: base-URL option
|
|
56
|
+
|
|
57
|
+
Set the client's own host option to postmock's REST listener, here `127.0.0.1:8080` over plain http.
|
|
58
|
+
|
|
59
|
+
| SDK | Option |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| postmark.js | `new ServerClient(token, { requestHost: "127.0.0.1:8080", useHttps: false })` |
|
|
62
|
+
| postmark-cli | `--request-host 127.0.0.1:8443` on `templates pull`, `templates push`, `email template`, `email raw` and `servers list`. The CLI keeps https: start postmock with `--https-port 8443` and a certificate from `tools/test-ca.sh`, and set `NODE_EXTRA_CA_CERTS` to its `ca.pem`. |
|
|
63
|
+
| postmark-python | `ServerClient(token, base_url="http://127.0.0.1:8080")` |
|
|
64
|
+
| postmark-dotnet | `new PostmarkClient(token, "http://127.0.0.1:8080")` |
|
|
65
|
+
| postmark-php | `PostmarkClientBase::$BASE_URL = "http://127.0.0.1:8080"` |
|
|
66
|
+
| postmark-gem, postmark-rails | `host: "127.0.0.1", port: 8080, secure: false` (rails: `config.action_mailer.postmark_settings`) |
|
|
67
|
+
| postmark-java | `Postmark.getApiClient(token, false, "127.0.0.1:8080")`: host and port, then the scheme from the `false` |
|
|
68
|
+
| postmark-mcp | none: use option B |
|
|
69
|
+
|
|
70
|
+
Cites and per-SDK transport details: [`docs/01`](docs/01-client-reachability.md) §2.1, [`docs/08`](docs/08-sdk-client-matrix.md).
|
|
71
|
+
|
|
72
|
+
### Sender addresses
|
|
73
|
+
|
|
74
|
+
As on Postmark, a send `From` must be on a DKIM-verified domain or be a confirmed sender signature.
|
|
75
|
+
A `From` that no account domain or signature matches gets HTTP 422 with ErrorCode 400 ([`docs/03`](docs/03-sending.md) §3.4, CAPTURED).
|
|
76
|
+
Other states that no capture covers answer 501 on REST and a 502 reply on SMTP: an unverified domain, a subdomain, an unconfirmed signature.
|
|
77
|
+
SMTP accepts every message and records problems as bounces (`refs/user-guide_send-email-with-smtp.md:56`); the `SMTPApiError` bounce with ErrorCode 400 is INFERRED.
|
|
78
|
+
The `POSTMARK_API_TEST` token skips this check.
|
|
79
|
+
|
|
80
|
+
| Need | How |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| Send from `example.com` | The `conformance` seed verifies `example.com` and `your-verified-domain.com` |
|
|
83
|
+
| Send from another domain | `POST /domains` with the account token, then `POST /control/domains/<ID>/verify` with `{"dkim": true}` ([`CONTROL-API.md`](CONTROL-API.md)) |
|
|
84
|
+
| Send from one address | `POST /senders` with the account token, then `POST /control/senders/<ID>/confirm` |
|
|
85
|
+
|
|
86
|
+
### Option B: DNS and a test CA, with Compose
|
|
87
|
+
|
|
88
|
+
[`compose.yaml`](compose.yaml) runs postmock on the internal network `sandbox` under the names `api.postmarkapp.com`, `smtp.postmarkapp.com` and `smtp-broadcasts.postmarkapp.com`.
|
|
89
|
+
postmock serves REST over TLS on 443 and SMTP with STARTTLS on 25, 587 and 2525.
|
|
90
|
+
|
|
91
|
+
| Volume | Content | Mounted by |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| `ca` | `ca.pem` only | clients |
|
|
94
|
+
| `tls` | `cert.pem`, `key.pem` | postmock only |
|
|
95
|
+
|
|
96
|
+
The `ca` service ([`tools/compose-ca.sh`](tools/compose-ca.sh)) writes both with [`tools/test-ca.sh`](tools/test-ca.sh).
|
|
97
|
+
It deletes the CA key after signing: that is the protection, as nobody can sign another certificate with this CA.
|
|
98
|
+
Name constraints on the CA (`postmarkapp.com`, `localhost`, `127.0.0.1`) add a limit only for clients that enforce them on a trust anchor; Java does not.
|
|
99
|
+
The volumes keep the CA across runs. A missing file, a key that does not match the certificate, or a certificate that fails to verify or expires within a day makes a new one; restart postmock after that (`docker compose restart postmock`).
|
|
100
|
+
|
|
101
|
+
Run the example application:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
docker compose run --rm --build example-node
|
|
105
|
+
docker compose --profile example down -v
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
[`examples/node/default-hosts.ts`](examples/node/default-hosts.ts) sends with postmark.js and nodemailer, and neither names postmock.
|
|
109
|
+
|
|
110
|
+
To test your own application, include this `compose.yaml` from your Compose file (Compose 2.20 or later).
|
|
111
|
+
[`examples/compose/compose.yaml`](examples/compose/compose.yaml) is a complete file:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
include:
|
|
115
|
+
- path:
|
|
116
|
+
- path/to/postmock/compose.yaml
|
|
117
|
+
- postmock.override.yaml # your settings for the postmock service
|
|
118
|
+
services:
|
|
119
|
+
app:
|
|
120
|
+
build: .
|
|
121
|
+
depends_on:
|
|
122
|
+
postmock: { condition: service_healthy }
|
|
123
|
+
environment:
|
|
124
|
+
NODE_EXTRA_CA_CERTS: /ca/ca.pem
|
|
125
|
+
POSTMARK_SERVER_TOKEN: postmock-server-token
|
|
126
|
+
volumes: [ca:/ca:ro]
|
|
127
|
+
networks: [sandbox]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
| Need | Setting |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| Trust the CA | Node: `NODE_EXTRA_CA_CERTS=/ca/ca.pem`; Java: import `ca.pem` into a trust store; others: the runtime's trust store |
|
|
133
|
+
| Read what was sent | `http://postmock:8025/control/messages` |
|
|
134
|
+
| Receive webhooks | `POSTMOCK_WEBHOOKS_ALLOW_HOSTS: app` in the override file ([`examples/compose/postmock.override.yaml`](examples/compose/postmock.override.yaml)) |
|
|
135
|
+
|
|
136
|
+
A service of your file cannot redefine an included service, so postmock settings go in the override file.
|
|
137
|
+
|
|
138
|
+
No route out holds only while the application joins `sandbox` and no other network.
|
|
139
|
+
With a second network, `api.postmarkapp.com` resolves to real Postmark whenever postmock is down.
|
|
140
|
+
Use a token that only postmock knows in every case: a misrouted request then gets 401 and sends nothing.
|
|
141
|
+
|
|
142
|
+
Why DNS and not a hosts file: nodemailer queries DNS before it reads the hosts file ([`docs/01`](docs/01-client-reachability.md) §2.2).
|
|
143
|
+
|
|
144
|
+
### SMTP
|
|
145
|
+
|
|
146
|
+
| Setting | Value |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| Host | postmock's host, or `smtp.postmarkapp.com` with option B |
|
|
149
|
+
| Port | The `smtp` URL in the startup line; `25`, `587` or `2525` with option B |
|
|
150
|
+
| User and password | A server API token for both, or an SMTP token from `POST /control/smtp/tokens` |
|
|
151
|
+
| TLS | STARTTLS when `POSTMOCK_SMTP_TLS_KEY` and `POSTMOCK_SMTP_TLS_CERT` are set |
|
|
152
|
+
| Stream | Header `X-PM-Message-Stream` |
|
|
153
|
+
|
|
154
|
+
[`examples/node/smtp-send.ts`](examples/node/smtp-send.ts) sends with nodemailer through option A.
|
|
155
|
+
Details: [`docs/07`](docs/07-smtp-sandbox-limits.md).
|
|
156
|
+
|
|
157
|
+
## Control API
|
|
158
|
+
|
|
159
|
+
Tests drive postmock through the control API on its own port: reset, seeds, the clock, faults, sent messages, bounces, opens, clicks, inbound mail and webhook attempts.
|
|
160
|
+
Every call maps to something that can happen on real Postmark.
|
|
161
|
+
Reference: [`CONTROL-API.md`](CONTROL-API.md).
|
|
162
|
+
|
|
163
|
+
## SDK compatibility
|
|
164
|
+
|
|
165
|
+
Each official SDK's own live integration suite runs unmodified against postmock ([`TESTING.md`](TESTING.md)).
|
|
166
|
+
`pnpm compat-table` writes this table from `conformance/results/*.json`.
|
|
167
|
+
`not run` means the suite has no results yet. postmark-cli, postmark-java and postmark-php run in Docker containers.
|
|
168
|
+
CI runs every suite, writes the full table to its job summary, and fails when a row this table lists as run differs from its results ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)).
|
|
169
|
+
A `not run` row enters this table only after a real run.
|
|
170
|
+
|
|
171
|
+
<!-- compat-table:start -->
|
|
172
|
+
| SDK | SDK commit | Pass | Fail | Skip | Baseline |
|
|
173
|
+
| --- | --- | --- | --- | --- | --- |
|
|
174
|
+
| postmark-cli | c88a59d | 31/31 | 0 | 0 | 31 |
|
|
175
|
+
| postmark-dotnet | b4249c5 | 103/111 | 0 | 8 | 103 |
|
|
176
|
+
| postmark-gem | a50ff39 | 41/41 | 0 | 0 | 41 |
|
|
177
|
+
| postmark-java | c6c5eb6 | 100/100 | 0 | 0 | 100 |
|
|
178
|
+
| postmark-mcp | 63ef055 | 50/50 | 0 | 0 | 50 |
|
|
179
|
+
| postmark-php | ad4b80e | 87/87 | 0 | 0 | 87 |
|
|
180
|
+
| postmark-python | 620d659 | 72/174 | 0 | 102 | 72 |
|
|
181
|
+
| postmark-rails | f9e4acc | 7/7 | 0 | 0 | 7 |
|
|
182
|
+
| postmark.js | f955212 | 79/80 | 0 | 1 | 79 |
|
|
183
|
+
<!-- compat-table:end -->
|
|
184
|
+
|
|
185
|
+
## Why
|
|
186
|
+
|
|
187
|
+
Postmark has no local test server.
|
|
188
|
+
Its sandbox mode is hosted: it cannot inject faults or fire events on demand, and sends count toward the plan.
|
|
189
|
+
Existing open-source mocks answer `POST /email` only, with no errors, suppressions, webhooks or SMTP (`docs/09`).
|
|
190
|
+
|
|
191
|
+
## Scope
|
|
192
|
+
|
|
193
|
+
| Surface | Doc |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| Transport, tokens, error envelope, ErrorCodes | `docs/02` |
|
|
196
|
+
| Sending: single, batch, templates, bulk | `docs/03` |
|
|
197
|
+
| Bounces, suppressions, message streams, data removals | `docs/04` |
|
|
198
|
+
| Webhooks: config API, every RecordType, retries, inbound | `docs/05` |
|
|
199
|
+
| Messages, stats, templates, server, servers, domains, sender signatures | `docs/06` |
|
|
200
|
+
| SMTP, sandbox mode, limits | `docs/07` |
|
|
201
|
+
|
|
202
|
+
## Top traps
|
|
203
|
+
|
|
204
|
+
| Trap | Doc |
|
|
205
|
+
| --- | --- |
|
|
206
|
+
| The 406 `Message` must match an SDK regex, or clients get an empty inactive-recipient list | `docs/03` |
|
|
207
|
+
| postmark.js has no proxy support and a default host; apps that never set a host need DNS + a test CA | `docs/01` |
|
|
208
|
+
| SMTP accepts every message; problems become `SMTPApiError` bounces, not SMTP rejects | `docs/07` |
|
|
209
|
+
| Every success is HTTP 200 with JSON; several SDKs treat 201/204 as errors | `docs/08` |
|
|
210
|
+
| SDKs disagree on path case, query key case and boolean encoding; the server must accept all of them | `docs/08` |
|
|
211
|
+
|
|
212
|
+
## Layout
|
|
213
|
+
|
|
214
|
+
| Path | Content |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| `AGENTS.md` | Rules for working in this repo (`CLAUDE.md` links to it) |
|
|
217
|
+
| `ARCHITECTURE.md` | Listeners, modules, request flow, shared contracts, traps |
|
|
218
|
+
| `CONTROL-API.md` | Test-facing control API: principle, shape, endpoints, seeds |
|
|
219
|
+
| `TESTING.md` | Gates, conformance runners, results and baseline, test traps |
|
|
220
|
+
| `flake.nix` | Dev shell with every toolchain the SDK suites need |
|
|
221
|
+
| `Dockerfile`, `compose.yaml` | The image, and the option B sandbox |
|
|
222
|
+
| `.github/workflows/` | CI |
|
|
223
|
+
| `examples/node/` | Applications that send through Postmark with no postmock code |
|
|
224
|
+
| `examples/compose/` | An application's Compose file that includes postmock's |
|
|
225
|
+
| `src/` | The server (`ARCHITECTURE.md` "Modules") |
|
|
226
|
+
| `seeds/` | Named seeds: `empty`, `conformance` |
|
|
227
|
+
| `conformance/` | One runner per SDK suite, baselines, skip lists |
|
|
228
|
+
| `docs/01-client-reachability.md` | How clients pick the host; routing an unmodified app to the mock; client parsing of server text; surface tiers |
|
|
229
|
+
| `docs/02-transport-auth-errors.md` | Hosts, headers, tokens, `POSTMARK_API_TEST`, error envelope, ErrorCode table, SDK error mapping, JSON and paging rules |
|
|
230
|
+
| `docs/03-sending.md` | `/email`, batch, templates, bulk; validation and ErrorCodes; sandbox; tracking after accept |
|
|
231
|
+
| `docs/04-bounces-suppressions-streams.md` | Bounce API, suppressions, message streams, data removals, and the state machine between them |
|
|
232
|
+
| `docs/05-webhooks.md` | Webhook config API, every RecordType payload, retries, inbound |
|
|
233
|
+
| `docs/06-messages-stats-templates-server.md` | Messages, opens/clicks, stats, templates, server/servers/domains/signatures |
|
|
234
|
+
| `docs/07-smtp-sandbox-limits.md` | SMTP and `X-PM-*` headers, sandbox mode, bounce-testing addresses, limits |
|
|
235
|
+
| `docs/08-sdk-client-matrix.md` | How each official SDK sends and parses; endpoint coverage; spec bugs; conformance plan |
|
|
236
|
+
| `docs/09-implementation-options.md` | TypeScript design, routing options, control API, phases, decisions |
|
|
237
|
+
| `docs/10-live-capture-plan.md` | Open questions and a safe capture harness design (deferred) |
|
|
238
|
+
| `docs/11-build-plan.md` | Waves, parallel tracks, conformance runners, definition of done |
|
|
239
|
+
| `tools/fetch-sources.sh` | Clones the official SDKs into `sdk/` at the commits the docs cite |
|
|
240
|
+
| `tools/fetch-refs.sh` | Downloads Postmark's docs and Swagger specs into `refs/` |
|
|
241
|
+
| `tools/test-ca.sh` | Writes a test CA and a certificate for the Postmark host names |
|
|
242
|
+
| `tools/compose-ca.sh` | Keeps a valid test CA in the Compose volumes |
|
|
243
|
+
| `tools/pack-smoke.sh` | Installs the packed npm package outside the repo and checks REST and SMTP |
|
|
244
|
+
| `tools/compat-table.ts` | Writes the SDK compatibility table into this README |
|
|
245
|
+
|
|
246
|
+
`sdk/` and `refs/` are git-ignored: they hold third-party code and Postmark's copyrighted docs.
|
|
247
|
+
Run both scripts before you follow a citation.
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
MIT.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { addAccountToken, createServer } from "../../src/state/servers.js";
|
|
2
|
+
import { CONFORMANCE } from "../lib/conformance.js";
|
|
3
|
+
/** Account token and server 1 with the default streams. */
|
|
4
|
+
const core = ({ store, clock }) => {
|
|
5
|
+
addAccountToken(store, CONFORMANCE.accountToken);
|
|
6
|
+
createServer(store, clock.now(), {
|
|
7
|
+
ID: CONFORMANCE.serverId,
|
|
8
|
+
Name: CONFORMANCE.serverName,
|
|
9
|
+
ApiTokens: [CONFORMANCE.serverToken],
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
export default core;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { BOUNCE_DESCRIPTIONS } from "../../src/recipients/transitions.js";
|
|
2
|
+
import { suppressionKey } from "../../src/state/store.js";
|
|
3
|
+
import { CONFORMANCE } from "../lib/conformance.js";
|
|
4
|
+
// The conformance server received two bounces: SDK suites read the first bounce, its dump, and activate an
|
|
5
|
+
// inactive one (docs/08 §5.3). The messages carry the bounced addresses, never the recipient that
|
|
6
|
+
// sending tests use. Bounce IDs come from the T2 range (docs/11 §5).
|
|
7
|
+
const MINUTE = 60_000;
|
|
8
|
+
const bounced = (now, id, messageId, email, type, details, minutesAgo) => {
|
|
9
|
+
const sentAt = new Date(now.getTime() - (minutesAgo + 1) * MINUTE);
|
|
10
|
+
const bouncedAt = new Date(now.getTime() - minutesAgo * MINUTE);
|
|
11
|
+
const subject = "postmock seeded bounce";
|
|
12
|
+
const message = {
|
|
13
|
+
MessageID: messageId,
|
|
14
|
+
ServerID: CONFORMANCE.serverId,
|
|
15
|
+
MessageStream: "outbound",
|
|
16
|
+
From: CONFORMANCE.senderEmail,
|
|
17
|
+
To: [{ Email: email, Name: null }],
|
|
18
|
+
Cc: [],
|
|
19
|
+
Bcc: [],
|
|
20
|
+
ReplyTo: null,
|
|
21
|
+
Subject: subject,
|
|
22
|
+
HtmlBody: null,
|
|
23
|
+
TextBody: "Hello",
|
|
24
|
+
Tag: "postmock-bounce",
|
|
25
|
+
Headers: [],
|
|
26
|
+
Attachments: [],
|
|
27
|
+
Metadata: {},
|
|
28
|
+
TrackOpens: false,
|
|
29
|
+
TrackLinks: "None",
|
|
30
|
+
Status: "Sent",
|
|
31
|
+
Sandboxed: false,
|
|
32
|
+
ReceivedAt: sentAt,
|
|
33
|
+
// docs/06 §1.6; a hard bounce also suppresses the address.
|
|
34
|
+
MessageEvents: [
|
|
35
|
+
{
|
|
36
|
+
Recipient: email,
|
|
37
|
+
Type: "Bounced",
|
|
38
|
+
ReceivedAt: bouncedAt,
|
|
39
|
+
Details: { Summary: details, BounceID: String(id) },
|
|
40
|
+
},
|
|
41
|
+
...(type === "HardBounce"
|
|
42
|
+
? [
|
|
43
|
+
{
|
|
44
|
+
Recipient: email,
|
|
45
|
+
Type: "SubscriptionChanged",
|
|
46
|
+
ReceivedAt: bouncedAt,
|
|
47
|
+
Details: { Origin: "Recipient", SuppressSending: "True" },
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
: []),
|
|
51
|
+
],
|
|
52
|
+
channel: "rest",
|
|
53
|
+
request: {
|
|
54
|
+
From: CONFORMANCE.senderEmail,
|
|
55
|
+
To: email,
|
|
56
|
+
Subject: subject,
|
|
57
|
+
TextBody: "Hello",
|
|
58
|
+
Tag: "postmock-bounce",
|
|
59
|
+
MessageStream: "outbound",
|
|
60
|
+
},
|
|
61
|
+
rawSource: "",
|
|
62
|
+
bulkRequestId: null,
|
|
63
|
+
templateId: null,
|
|
64
|
+
suppressedRecipients: [],
|
|
65
|
+
};
|
|
66
|
+
const bounce = {
|
|
67
|
+
ID: id,
|
|
68
|
+
ServerID: CONFORMANCE.serverId,
|
|
69
|
+
MessageStream: "outbound",
|
|
70
|
+
MessageID: messageId,
|
|
71
|
+
Type: type,
|
|
72
|
+
Tag: message.Tag,
|
|
73
|
+
Description: BOUNCE_DESCRIPTIONS[type],
|
|
74
|
+
Details: details,
|
|
75
|
+
Email: email,
|
|
76
|
+
From: CONFORMANCE.senderEmail,
|
|
77
|
+
Subject: subject,
|
|
78
|
+
BouncedAt: bouncedAt,
|
|
79
|
+
Inactive: type === "HardBounce",
|
|
80
|
+
CanActivate: true,
|
|
81
|
+
Content: `Return-Path: <>\r\nSubject: Undelivered Mail Returned to Sender\r\n\r\n${details}\r\n`,
|
|
82
|
+
Metadata: {},
|
|
83
|
+
};
|
|
84
|
+
return [message, bounce];
|
|
85
|
+
};
|
|
86
|
+
const bounces = ({ store, clock }) => {
|
|
87
|
+
const now = clock.now();
|
|
88
|
+
const seeded = [
|
|
89
|
+
bounced(now, 2001, "6f1c2a0e-2b7d-4c55-9a51-0c3f2d7e2001", "hardbounce@example.com", "HardBounce", "smtp;550 5.1.1 The email account does not exist.", 120),
|
|
90
|
+
bounced(now, 2002, "6f1c2a0e-2b7d-4c55-9a51-0c3f2d7e2002", "softbounce@example.com", "SoftBounce", "smtp;452 4.2.2 The mailbox is full.", 60),
|
|
91
|
+
];
|
|
92
|
+
for (const [message, bounce] of seeded) {
|
|
93
|
+
store.useId("bounce", bounce.ID);
|
|
94
|
+
store.state.outbound.set(message.MessageID, message);
|
|
95
|
+
store.state.bounces.set(bounce.ID, bounce);
|
|
96
|
+
if (bounce.Inactive) {
|
|
97
|
+
store.state.suppressions.set(suppressionKey(bounce.ServerID, bounce.MessageStream, bounce.Email), {
|
|
98
|
+
ServerID: bounce.ServerID,
|
|
99
|
+
MessageStream: bounce.MessageStream,
|
|
100
|
+
EmailAddress: bounce.Email,
|
|
101
|
+
SuppressionReason: "HardBounce",
|
|
102
|
+
Origin: "Recipient",
|
|
103
|
+
CreatedAt: bounce.BouncedAt,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
export default bounces;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { createServer } from "../../src/state/servers.js";
|
|
2
|
+
import { recordClick, recordDelivery, recordOpen } from "../../src/tracking.js";
|
|
3
|
+
import { CONFORMANCE } from "../lib/conformance.js";
|
|
4
|
+
import { pastBounce, pastInbound, pastSend, pastSmtpApiError } from "../lib/history.js";
|
|
5
|
+
import { READ_SERVER } from "../lib/read-server.js";
|
|
6
|
+
// Message history on the read server for the read suites (docs/08 §5.3; T4 ID range 4000–4999):
|
|
7
|
+
// ≥ 51 outbound messages in the retention window, some tagged `test_tag`, with deliveries,
|
|
8
|
+
// bounces, opens and clicks; ≥ 10 processed inbound messages among several statuses; and older
|
|
9
|
+
// sends that make the stats windows of the dotnet live test decrease strictly
|
|
10
|
+
// (sdk/postmark-dotnet/src/Postmark.Tests/ClientStatisticsTests.cs:40-62).
|
|
11
|
+
// php reads outbound message 51 and ten processed inbound messages
|
|
12
|
+
// (sdk/postmark-php/tests/PostmarkClientOutboundMessageTest.php:31;
|
|
13
|
+
// sdk/postmark-php/tests/PostmarkClientInboundMessageTest.php:21-24).
|
|
14
|
+
// Bounces 4000–4003: hard (suppresses reader-4), soft, transient, SMTP API error.
|
|
15
|
+
// The core conformance server gets one processed inbound message only: postmark.js reads inbound
|
|
16
|
+
// details there (sdk/postmark.js/test/integration/Messages.test.ts:119-133).
|
|
17
|
+
const HOUR = 60 * 60 * 1000;
|
|
18
|
+
const DAY = 24 * HOUR;
|
|
19
|
+
const LINK = "https://example.com/welcome";
|
|
20
|
+
const CLIENTS = [
|
|
21
|
+
{ Name: "Apple Mail 16.0", Company: "Apple Inc.", Family: "Apple Mail" },
|
|
22
|
+
{ Name: "Outlook 2019", Company: "Microsoft Corporation", Family: "Outlook" },
|
|
23
|
+
{ Name: "Chrome 120.0", Company: "Google Inc.", Family: "Chrome" },
|
|
24
|
+
{ Name: "Safari 17.1", Company: "Apple Inc.", Family: "Safari" },
|
|
25
|
+
];
|
|
26
|
+
/** A mail client for opens (`browser` false) or a browser for clicks. */
|
|
27
|
+
const agent = (n, browser) => ({
|
|
28
|
+
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15",
|
|
29
|
+
Client: CLIENTS[(browser ? 2 : 0) + (n % 3 === 0 ? 1 : 0)] ?? null,
|
|
30
|
+
OS: { Name: "OS X 10.15 Catalina", Company: "Apple Computer, Inc.", Family: "OS X" },
|
|
31
|
+
Platform: n % 3 === 0 ? "Mobile" : "Desktop",
|
|
32
|
+
Geo: { CountryISOCode: "DK", Country: "Denmark", City: "Copenhagen" },
|
|
33
|
+
});
|
|
34
|
+
const send = (runtime, at, fields) => pastSend(runtime, {
|
|
35
|
+
ServerID: READ_SERVER.id,
|
|
36
|
+
ReceivedAt: at,
|
|
37
|
+
From: CONFORMANCE.senderEmail,
|
|
38
|
+
To: fields.to.map((Email) => ({ Email, Name: null })),
|
|
39
|
+
Cc: (fields.cc ?? []).map((Email) => ({ Email, Name: null })),
|
|
40
|
+
Subject: fields.subject,
|
|
41
|
+
HtmlBody: `<p>${fields.subject}</p><p><a href="${LINK}">Start</a></p>`,
|
|
42
|
+
TextBody: `${fields.subject}\n\nStart: ${LINK}`,
|
|
43
|
+
Tag: fields.tag,
|
|
44
|
+
Metadata: { seed: "t4" },
|
|
45
|
+
TrackOpens: fields.tracked,
|
|
46
|
+
TrackLinks: fields.tracked ? "HtmlAndText" : "None",
|
|
47
|
+
});
|
|
48
|
+
const messages = async (runtime) => {
|
|
49
|
+
createServer(runtime.store, runtime.clock.now(), {
|
|
50
|
+
ID: READ_SERVER.id,
|
|
51
|
+
Name: READ_SERVER.name,
|
|
52
|
+
ApiTokens: [READ_SERVER.token],
|
|
53
|
+
});
|
|
54
|
+
const now = runtime.clock.now().getTime();
|
|
55
|
+
const ago = (ms) => new Date(now - ms);
|
|
56
|
+
const recent = [];
|
|
57
|
+
for (let i = 0; i < 40; i++) {
|
|
58
|
+
recent.push(await send(runtime, ago(i * 15 * HOUR + HOUR), {
|
|
59
|
+
to: [`reader-${i % 5}@example.com`],
|
|
60
|
+
...(i % 7 === 0 && { cc: ["reader-cc@example.com"] }),
|
|
61
|
+
subject: `Order update ${i}`,
|
|
62
|
+
tag: i % 3 === 0 ? "test_tag" : null,
|
|
63
|
+
tracked: i % 4 !== 3,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
// 26–29 days ago: after the dotnet `todate` of 30 days ago (ClientStatisticsTests.cs:16), so only
|
|
67
|
+
// the all-time window grows.
|
|
68
|
+
for (let i = 0; i < 8; i++) {
|
|
69
|
+
await send(runtime, ago(26 * DAY + i * 9 * HOUR), {
|
|
70
|
+
to: [`reader-${i % 5}@example.com`],
|
|
71
|
+
subject: `Monthly digest ${i}`,
|
|
72
|
+
tag: null,
|
|
73
|
+
tracked: false,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// Older sends: the stats windows [35 days ago, 30 days ago] and before (see header).
|
|
77
|
+
for (const [days, tag] of [
|
|
78
|
+
[32, "test_tag"],
|
|
79
|
+
[32, null],
|
|
80
|
+
[33, null],
|
|
81
|
+
[40, null],
|
|
82
|
+
[41, null],
|
|
83
|
+
[60, null],
|
|
84
|
+
]) {
|
|
85
|
+
await send(runtime, ago(days * DAY), {
|
|
86
|
+
to: ["reader-archive@example.com"],
|
|
87
|
+
subject: "Archived notice",
|
|
88
|
+
tag,
|
|
89
|
+
tracked: false,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const minutes = (m, n) => new Date(m.ReceivedAt.getTime() + n * 60000);
|
|
93
|
+
const hard = recent[4];
|
|
94
|
+
await pastBounce(runtime, hard, { id: 4000, type: "HardBounce", at: minutes(hard, 60) });
|
|
95
|
+
const soft = recent[8];
|
|
96
|
+
await pastBounce(runtime, soft, { id: 4001, type: "SoftBounce", at: minutes(soft, 60) });
|
|
97
|
+
// A delay notice comes before the delivery below.
|
|
98
|
+
const delayed = recent[12];
|
|
99
|
+
await pastBounce(runtime, delayed, { id: 4002, type: "Transient", at: minutes(delayed, 5) });
|
|
100
|
+
// A later SMTP send to the address the hard bounce suppressed.
|
|
101
|
+
await pastSmtpApiError(runtime, {
|
|
102
|
+
id: 4003,
|
|
103
|
+
serverId: READ_SERVER.id,
|
|
104
|
+
stream: "outbound",
|
|
105
|
+
email: hard.To[0]?.Email,
|
|
106
|
+
tag: null,
|
|
107
|
+
at: minutes(hard, 120),
|
|
108
|
+
});
|
|
109
|
+
for (const [i, message] of recent.entries()) {
|
|
110
|
+
if (message === hard || message === soft)
|
|
111
|
+
continue;
|
|
112
|
+
const later = (n) => minutes(message, n);
|
|
113
|
+
const target = { messageId: message.MessageID, recipient: message.To[0]?.Email };
|
|
114
|
+
await recordDelivery(runtime, { ...target, details: "smtp;250 2.0.0 OK" }, later(15));
|
|
115
|
+
if (!message.TrackOpens || i % 2 === 1)
|
|
116
|
+
continue;
|
|
117
|
+
await recordOpen(runtime, { ...target, agent: agent(i, false), readSeconds: 5 + i }, later(30));
|
|
118
|
+
if (i % 4 !== 0)
|
|
119
|
+
continue;
|
|
120
|
+
await recordOpen(runtime, { ...target, agent: agent(i, false), readSeconds: 3 }, later(45));
|
|
121
|
+
const location = i % 8 === 0 ? "HTML" : "Text";
|
|
122
|
+
await recordClick(runtime, { ...target, link: LINK, location, agent: agent(i, true) }, later(45));
|
|
123
|
+
}
|
|
124
|
+
const statuses = [
|
|
125
|
+
...Array(3).fill("Processed"),
|
|
126
|
+
"Blocked",
|
|
127
|
+
"Failed",
|
|
128
|
+
...Array(7).fill("Processed"),
|
|
129
|
+
];
|
|
130
|
+
for (const [n, Status] of statuses.entries()) {
|
|
131
|
+
pastInbound(runtime, {
|
|
132
|
+
ServerID: READ_SERVER.id,
|
|
133
|
+
ReceivedAt: ago((n + 1) * 6 * HOUR),
|
|
134
|
+
Status,
|
|
135
|
+
Subject: `Inbound ${n}`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
pastInbound(runtime, {
|
|
139
|
+
ServerID: CONFORMANCE.serverId,
|
|
140
|
+
ReceivedAt: ago(2 * HOUR),
|
|
141
|
+
Status: "Processed",
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
export default messages;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Seed } from "../../src/control/seed.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The conformance server is yellow, the color the dotnet server test leaves after its reset. That
|
|
4
|
+
* test sets purple and asserts the color changed
|
|
5
|
+
* (sdk/postmark-dotnet/src/Postmark.Tests/ClientServerInformationTests.cs:76-98).
|
|
6
|
+
*/
|
|
7
|
+
declare const serverColor: Seed;
|
|
8
|
+
export default serverColor;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CONFORMANCE } from "../lib/conformance.js";
|
|
2
|
+
/**
|
|
3
|
+
* The conformance server is yellow, the color the dotnet server test leaves after its reset. That
|
|
4
|
+
* test sets purple and asserts the color changed
|
|
5
|
+
* (sdk/postmark-dotnet/src/Postmark.Tests/ClientServerInformationTests.cs:76-98).
|
|
6
|
+
*/
|
|
7
|
+
const serverColor = ({ store }) => {
|
|
8
|
+
const server = store.state.servers.get(CONFORMANCE.serverId);
|
|
9
|
+
if (server === undefined)
|
|
10
|
+
throw new Error("the core seed part creates the conformance server");
|
|
11
|
+
server.Color = "yellow";
|
|
12
|
+
};
|
|
13
|
+
export default serverColor;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Seed } from "../../src/control/seed.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The account the SDK suites ran against: server deletion enabled, verified domains, and a
|
|
4
|
+
* confirmed sender signature. The dotnet, java and php suites read the first domain and signature
|
|
5
|
+
* (sdk/postmark-java/src/test/java/integration/DomainTest.java:36-41). A send needs a `From` on a
|
|
6
|
+
* verified domain or a confirmed signature (docs/03 §3.4).
|
|
7
|
+
*/
|
|
8
|
+
declare const account: Seed;
|
|
9
|
+
export default account;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { confirmDkim } from "../../src/api/account/authentication.js";
|
|
2
|
+
import { addVerifiedDomain } from "../../src/api/account/domains.js";
|
|
3
|
+
import { newSender } from "../../src/api/account/senders.js";
|
|
4
|
+
import { CONFORMANCE } from "../lib/conformance.js";
|
|
5
|
+
/**
|
|
6
|
+
* The account the SDK suites ran against: server deletion enabled, verified domains, and a
|
|
7
|
+
* confirmed sender signature. The dotnet, java and php suites read the first domain and signature
|
|
8
|
+
* (sdk/postmark-java/src/test/java/integration/DomainTest.java:36-41). A send needs a `From` on a
|
|
9
|
+
* verified domain or a confirmed signature (docs/03 §3.4).
|
|
10
|
+
*/
|
|
11
|
+
const account = ({ store, clock }) => {
|
|
12
|
+
const now = clock.now();
|
|
13
|
+
store.state.account.serverDeletionEnabled = true;
|
|
14
|
+
addVerifiedDomain(store.state, store.useId("domain", 7000), CONFORMANCE.domain, now);
|
|
15
|
+
// The postmark-python start example sends from its placeholder domain
|
|
16
|
+
// (sdk/postmark-python/examples/start_here.py:53); its owner verifies that domain first.
|
|
17
|
+
addVerifiedDomain(store.state, store.useId("domain", 7001), CONFORMANCE.exampleDomain, now);
|
|
18
|
+
const sender = newSender(store.useId("sender", 7000), {
|
|
19
|
+
FromEmail: CONFORMANCE.senderEmail,
|
|
20
|
+
Name: "postmock sender",
|
|
21
|
+
ReplyToEmail: "",
|
|
22
|
+
ReturnPathDomain: "",
|
|
23
|
+
ConfirmationPersonalNote: "",
|
|
24
|
+
}, now);
|
|
25
|
+
sender.Confirmed = true;
|
|
26
|
+
confirmDkim(sender);
|
|
27
|
+
store.state.senders.set(sender.ID, sender);
|
|
28
|
+
};
|
|
29
|
+
export default account;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createServer } from "../../src/state/servers.js";
|
|
2
|
+
import { TEMPLATE_SERVER } from "../lib/template-server.js";
|
|
3
|
+
/** A server with one standard template that renders without a model. */
|
|
4
|
+
const templates = ({ store, clock }) => {
|
|
5
|
+
createServer(store, clock.now(), {
|
|
6
|
+
ID: TEMPLATE_SERVER.id,
|
|
7
|
+
Name: TEMPLATE_SERVER.name,
|
|
8
|
+
ApiTokens: [TEMPLATE_SERVER.token],
|
|
9
|
+
});
|
|
10
|
+
store.state.templates.set(store.useId("template", TEMPLATE_SERVER.templateId), {
|
|
11
|
+
TemplateId: TEMPLATE_SERVER.templateId,
|
|
12
|
+
ServerID: TEMPLATE_SERVER.id,
|
|
13
|
+
Name: "postmock welcome",
|
|
14
|
+
Alias: "postmock-welcome",
|
|
15
|
+
Subject: "Welcome",
|
|
16
|
+
HtmlBody: "<p>Welcome</p>",
|
|
17
|
+
TextBody: "Welcome",
|
|
18
|
+
TemplateType: "Standard",
|
|
19
|
+
LayoutTemplate: null,
|
|
20
|
+
Active: true,
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
export default templates;
|