@groundmail/groundhog 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,9 +1,9 @@
1
- <p align="center"><img src="docs/groundhog.svg" width="360" alt="The Groundhog mascot peeking out of its burrow holding an envelope"></p>
1
+ <p align="center"><img src="docs/groundhog.svg" width="360" alt="Groundbot, the robot groundhog, peeking out of its burrow with happy eyes and an envelope on its chest light"></p>
2
2
 
3
3
  <h1 align="center">Groundhog</h1>
4
4
 
5
5
  <p align="center">Catch every email your app sends while you build. Nothing leaves your laptop.<br>
6
- An open-source local inbox from <a href="https://groundmail.dev/?utm_source=groundhog&utm_medium=readme&utm_campaign=header">Groundmail</a>.</p>
6
+ An open-source local inbox from <a href="https://groundmail.dev/?utm_source=github&utm_medium=readme&utm_campaign=groundhog&utm_content=header">Groundmail</a>.</p>
7
7
 
8
8
  ---
9
9
 
@@ -23,6 +23,10 @@ email it sends shows up at `http://localhost:3025`: sign-up codes, password rese
23
23
  - **For tests and agents**: `waitForEmail()` for Playwright/Vitest, and an MCP server so coding
24
24
  agents can read the verification email themselves.
25
25
 
26
+ <p align="center"><img src="docs/screenshot-inbox.png" width="900" alt="The Groundhog inbox: three caught emails (a Resend sign-up code, an SMTP password reset and a SendGrid receipt), with the verification email open and its one-time code 482913 and confirm link pulled out above the rendered HTML"></p>
27
+
28
+ <p align="center"><a href="docs/demo.gif">Watch a 15-second demo</a></p>
29
+
26
30
  Coming from MailHog? Groundhog keeps SMTP on 1025; the inbox moves from 8025 to 3025.
27
31
 
28
32
  ## Start it
@@ -228,6 +232,8 @@ curl -X POST localhost:3025/api/faults -d '{"status":429,"next":3}'
228
232
 
229
233
  A banner stays in the inbox while failure mode is on. `next: 0` or `DELETE /api/faults` turns it off.
230
234
 
235
+ <p align="center"><img src="docs/screenshot-failure-mode.png" width="760" alt="The Failure mode dialog: pick rate limited (429), invalid recipient (422), bad API key (401), provider outage (5xx) or a 30-second timeout, each with its SMTP reply code, and how many sends should fail"></p>
236
+
231
237
  ## Webhook simulation
232
238
 
233
239
  Set your app's webhook URL in an email's **Webhooks** tab (or `PUT /api/webhooks`), then fire
@@ -252,7 +258,7 @@ public; never use them outside local development.
252
258
  ## Send to my real inbox
253
259
 
254
260
  Some things only show up in a real mail client. Open an email, click **Send to my real inbox**,
255
- and connect a free [Groundmail account](https://app.groundmail.dev/signup?utm_source=groundhog&utm_medium=readme&utm_campaign=real_inbox)
261
+ and connect a free [Groundmail account](https://app.groundmail.dev/signup?utm_source=github&utm_medium=readme&utm_campaign=groundhog&utm_content=real_inbox)
256
262
  once. The sign-in opens in your browser; there's no key to paste. Copies come from Groundmail's
257
263
  sandbox sender, which only delivers to your own account address until you verify a domain, so
258
264
  Groundhog can't email real users by mistake. The sign-in is saved in `~/.config/groundhog` and
@@ -293,7 +299,7 @@ Groundhog never relays mail. Nothing it catches is sent anywhere unless you clic
293
299
 
294
300
  ## Ready for real inboxes?
295
301
 
296
- When it's time to ship, [Groundmail](https://groundmail.dev/?utm_source=groundhog&utm_medium=readme&utm_campaign=go_live)
302
+ When it's time to ship, [Groundmail](https://groundmail.dev/?utm_source=github&utm_medium=readme&utm_campaign=groundhog&utm_content=go_live)
297
303
  sends your app's email for real: an HTTP API and SMTP, with transactional mail on its own servers
298
304
  and IPs. The free plan includes 3,000 emails a month. If you use the Groundmail SDK, going live is
299
305
  two environment variables:
@@ -302,7 +308,7 @@ two environment variables:
302
308
  GROUNDMAIL_API_KEY=gm_live_… # and drop GROUNDMAIL_BASE_URL
303
309
  ```
304
310
 
305
- [Start free on Groundmail →](https://app.groundmail.dev/signup?utm_source=groundhog&utm_medium=readme&utm_campaign=go_live)
311
+ [Start free on Groundmail →](https://app.groundmail.dev/signup?utm_source=github&utm_medium=readme&utm_campaign=groundhog&utm_content=go_live)
306
312
 
307
313
  ## Development
308
314
 
@@ -314,7 +320,10 @@ npx playwright install chromium
314
320
  npm run test:e2e # the inbox UI and the Playwright example
315
321
  ```
316
322
 
317
- Issues and pull requests are welcome.
323
+ Releases are automatic: bump `version` in `package.json` and merge to `main`. The release
324
+ workflow publishes to npm (with provenance), tags the version, and pushes the Docker image.
325
+
326
+ Issues and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). Report security issues as described in [SECURITY.md](SECURITY.md).
318
327
 
319
328
  ## License
320
329
 
package/dist/ui/app.js CHANGED
@@ -27,10 +27,12 @@ function svg(markup) {
27
27
  return template.content.firstElementChild
28
28
  }
29
29
 
30
+ // Groundbot, the robot groundhog: asleep while the inbox is empty, happy when an email is caught,
31
+ // crossed-out eyes when a send fails.
30
32
  const MASCOT = {
31
- dozing: `<svg class="empty-art" viewBox="0 0 360 200" width="360" height="200" role="img" aria-label="The groundhog dozing in its burrow"><rect x="0" y="150" width="360" height="14" fill="#4a3a29"/><rect x="0" y="164" width="360" height="12" fill="#3a2d20"/><rect x="0" y="176" width="360" height="12" fill="#2c2218"/><rect x="0" y="188" width="360" height="12" fill="#1f1811"/><ellipse cx="180" cy="152" rx="96" ry="18" fill="#0d0a07"/><path d="M112 152 C112 104 140 84 180 84 C220 84 248 104 248 152 Z" fill="#8b5a33"/><circle cx="146" cy="92" r="11" fill="#8b5a33"/><circle cx="214" cy="92" r="11" fill="#8b5a33"/><path d="M152 122 q10 -8 20 0" fill="none" stroke="#241c15" stroke-width="4" stroke-linecap="round"/><path d="M188 122 q10 -8 20 0" fill="none" stroke="#241c15" stroke-width="4" stroke-linecap="round"/><ellipse cx="180" cy="138" rx="18" ry="11" fill="#e8c9a0"/><ellipse cx="180" cy="134" rx="6" ry="4" fill="#241c15"/><text x="262" y="78" font-size="20" fill="#7d6e5f" font-family="ui-monospace, Menlo, monospace">z</text><text x="280" y="58" font-size="16" fill="#7d6e5f" font-family="ui-monospace, Menlo, monospace">z</text><text x="294" y="42" font-size="12" fill="#7d6e5f" font-family="ui-monospace, Menlo, monospace">z</text></svg>`,
32
- mail: `<svg viewBox="0 0 60 44" width="48" height="36" aria-hidden="true"><ellipse cx="30" cy="40" rx="24" ry="4" fill="#0d0a07"/><path d="M14 40 C14 18 20 6 30 6 C40 6 46 18 46 40 Z" fill="#8b5a33"/><circle cx="25" cy="18" r="2.4" fill="#241c15"/><circle cx="35" cy="18" r="2.4" fill="#241c15"/><rect x="20" y="25" width="20" height="13" rx="2" fill="#fbf8f1"/><path d="M21 26 L30 32 L39 26" fill="none" stroke="#a3461f" stroke-width="1.6"/></svg>`,
33
- shadow: `<svg viewBox="0 0 120 70" width="56" height="34" aria-hidden="true"><ellipse cx="60" cy="62" rx="46" ry="8" fill="#0d0a07"/><ellipse cx="92" cy="62" rx="22" ry="5" fill="#8b6643" opacity="0.6"/><path d="M30 62 C30 26 42 8 60 8 C78 8 90 26 90 62 Z" fill="#8b5a33"/><circle cx="50" cy="28" r="5" fill="#fbf8f1" stroke="#241c15" stroke-width="2"/><circle cx="70" cy="28" r="5" fill="#fbf8f1" stroke="#241c15" stroke-width="2"/><circle cx="50" cy="28" r="2" fill="#241c15"/><circle cx="70" cy="28" r="2" fill="#241c15"/><ellipse cx="60" cy="42" rx="5" ry="4" fill="#241c15"/></svg>`,
33
+ dozing: `<svg class="empty-art" viewBox="0 0 360 200" width="360" height="200" role="img" aria-label="Groundbot asleep in its burrow"><rect x="0" y="150" width="360" height="14" fill="#4a3a29"/><rect x="0" y="164" width="360" height="12" fill="#3a2d20"/><rect x="0" y="176" width="360" height="12" fill="#2c2218"/><rect x="0" y="188" width="360" height="12" fill="#1f1811"/><ellipse cx="180" cy="152" rx="96" ry="18" fill="#0d0a07"/><g transform="translate(95 9.2) scale(0.85)"><rect x="88" y="6" width="5" height="22" fill="#241c15"/><circle cx="90.5" cy="8" r="7" fill="#5f432c" stroke="#241c15" stroke-width="3"/><rect x="34" y="30" width="22" height="26" rx="6" fill="#5f432c" stroke="#241c15" stroke-width="4"/><rect x="144" y="30" width="22" height="26" rx="6" fill="#5f432c" stroke="#241c15" stroke-width="4"/><circle cx="45" cy="43" r="3" fill="#d6bf98"/><circle cx="155" cy="43" r="3" fill="#d6bf98"/><rect x="44" y="26" width="112" height="104" rx="26" fill="#8b6643" stroke="#241c15" stroke-width="4"/><rect x="58" y="50" width="84" height="44" rx="12" fill="#0d0a07" stroke="#241c15" stroke-width="3"/><path d="M72 74 h20 M108 74 h20" fill="none" stroke="#5f432c" stroke-width="5" stroke-linecap="round"/><text x="150" y="22" font-family="Geist, ui-sans-serif, system-ui, sans-serif" font-size="18" font-weight="700" fill="#8b6643">z</text><text x="164" y="10" font-family="Geist, ui-sans-serif, system-ui, sans-serif" font-size="13" font-weight="700" fill="#b48f62">z</text><rect x="78" y="98" width="44" height="22" rx="10" fill="#e8c9a0" stroke="#241c15" stroke-width="3"/><rect x="94" y="100" width="12" height="7" rx="3" fill="#241c15"/><rect x="93" y="116" width="6" height="9" rx="1" fill="#fbf8f1" stroke="#241c15" stroke-width="2"/><rect x="101" y="116" width="6" height="9" rx="1" fill="#fbf8f1" stroke="#241c15" stroke-width="2"/><path d="M52 104h12M52 112h12M136 104h12M136 112h12" stroke="#5f432c" stroke-width="3" stroke-linecap="round"/><rect x="30" y="128" width="140" height="40" rx="8" fill="#241c15"/><rect x="84" y="136" width="32" height="22" rx="4" fill="#5f432c"/><path d="M87 139 L100 149 L113 139" fill="none" stroke="#241c15" stroke-width="2.5" stroke-linejoin="round"/></g></svg>`,
34
+ mail: `<svg viewBox="0 0 60 44" width="48" height="36" aria-hidden="true"><g transform="translate(5 1) scale(0.25)"><rect x="87.5" y="6" width="6" height="22" fill="#241c15"/><circle cx="90.5" cy="8" r="8" fill="#e27d4e" stroke="#241c15" stroke-width="3"/><rect x="34" y="30" width="22" height="26" rx="6" fill="#5f432c" stroke="#241c15" stroke-width="5"/><rect x="144" y="30" width="22" height="26" rx="6" fill="#5f432c" stroke="#241c15" stroke-width="5"/><circle cx="45" cy="43" r="4" fill="#d6bf98"/><circle cx="155" cy="43" r="4" fill="#d6bf98"/><rect x="44" y="26" width="112" height="104" rx="26" fill="#8b6643" stroke="#241c15" stroke-width="6"/><rect x="58" y="50" width="84" height="44" rx="12" fill="#0d0a07"/><path d="M71 78 Q82 62 93 78 M107 78 Q118 62 129 78" fill="none" stroke="#e9b48a" stroke-width="8" stroke-linecap="round"/><rect x="78" y="98" width="44" height="22" rx="10" fill="#e8c9a0" stroke="#241c15" stroke-width="3"/><rect x="94" y="100" width="12" height="7" rx="3" fill="#241c15"/><rect x="93" y="116" width="14" height="9" rx="1" fill="#fbf8f1" stroke="#241c15" stroke-width="2"/><rect x="30" y="128" width="140" height="40" rx="8" fill="#241c15"/><rect x="84" y="136" width="32" height="22" rx="4" fill="#9cb67f"/><path d="M87 139 L100 149 L113 139" fill="none" stroke="#241c15" stroke-width="2.5" stroke-linejoin="round"/></g></svg>`,
35
+ shadow: `<svg viewBox="0 0 120 70" width="56" height="34" aria-hidden="true"><g transform="translate(20.714 2) scale(0.393)"><rect x="87.5" y="6" width="6" height="22" fill="#241c15"/><circle cx="90.5" cy="8" r="8" fill="#e27d4e" stroke="#241c15" stroke-width="3"/><rect x="34" y="30" width="22" height="26" rx="6" fill="#5f432c" stroke="#241c15" stroke-width="5"/><rect x="144" y="30" width="22" height="26" rx="6" fill="#5f432c" stroke="#241c15" stroke-width="5"/><circle cx="45" cy="43" r="4" fill="#d6bf98"/><circle cx="155" cy="43" r="4" fill="#d6bf98"/><rect x="44" y="26" width="112" height="104" rx="26" fill="#8b6643" stroke="#241c15" stroke-width="6"/><rect x="58" y="50" width="84" height="44" rx="12" fill="#0d0a07"/><path d="M75 65 L89 79 M89 65 L75 79 M111 65 L125 79 M125 65 L111 79" fill="none" stroke="#e27d4e" stroke-width="8" stroke-linecap="round"/><rect x="78" y="98" width="44" height="22" rx="10" fill="#e8c9a0" stroke="#241c15" stroke-width="3"/><rect x="94" y="100" width="12" height="7" rx="3" fill="#241c15"/><rect x="93" y="116" width="14" height="9" rx="1" fill="#fbf8f1" stroke="#241c15" stroke-width="2"/><rect x="30" y="128" width="140" height="40" rx="8" fill="#241c15"/><rect x="84" y="136" width="32" height="22" rx="4" fill="#e27d4e"/><path d="M87 139 L100 149 L113 139" fill="none" stroke="#241c15" stroke-width="2.5" stroke-linejoin="round"/></g></svg>`,
34
36
  }
35
37
 
36
38
  const VIA = {
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><rect width="40" height="40" rx="8" fill="#8b5a33"/><ellipse cx="20" cy="33" rx="14" ry="3.5" fill="#1f1811"/><path d="M10 33 C10 18 14 11 20 11 C26 11 30 18 30 33 Z" fill="#e8c9a0"/><circle cx="16.5" cy="19" r="1.8" fill="#241c15"/><circle cx="23.5" cy="19" r="1.8" fill="#241c15"/><ellipse cx="20" cy="23" rx="2" ry="1.4" fill="#241c15"/></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><g transform="translate(-8.358 1) scale(0.284)"><rect x="88" y="6" width="5" height="22" fill="#241c15"/><circle cx="90.5" cy="8" r="8" fill="#e27d4e"/><rect x="44" y="26" width="112" height="104" rx="26" fill="#8b6643" stroke="#241c15" stroke-width="8"/><rect x="58" y="50" width="84" height="44" rx="12" fill="#0d0a07"/><circle cx="82" cy="72" r="9" fill="#e9b48a"/><circle cx="118" cy="72" r="9" fill="#e9b48a"/><rect x="88" y="110" width="24" height="18" rx="3" fill="#fbf8f1" stroke="#241c15" stroke-width="5"/></g></svg>
@@ -12,7 +12,7 @@
12
12
  <div class="app">
13
13
  <header class="top">
14
14
  <div class="brand">
15
- <svg viewBox="0 0 40 40" width="32" height="32" aria-hidden="true"><rect width="40" height="40" rx="8" fill="#8b5a33"/><ellipse cx="20" cy="33" rx="14" ry="3.5" fill="#1f1811"/><path d="M10 33 C10 18 14 11 20 11 C26 11 30 18 30 33 Z" fill="#e8c9a0"/><circle cx="16.5" cy="19" r="1.8" fill="#241c15"/><circle cx="23.5" cy="19" r="1.8" fill="#241c15"/><ellipse cx="20" cy="23" rx="2" ry="1.4" fill="#241c15"/></svg>
15
+ <svg viewBox="0 0 40 40" width="32" height="32" aria-hidden="true"><g transform="translate(-7.737 1.555) scale(0.277)"><rect x="87.5" y="6" width="6" height="22" fill="#241c15"/><circle cx="90.5" cy="8" r="8" fill="#e27d4e" stroke="#241c15" stroke-width="3"/><rect x="34" y="30" width="22" height="26" rx="6" fill="#5f432c" stroke="#241c15" stroke-width="5"/><rect x="144" y="30" width="22" height="26" rx="6" fill="#5f432c" stroke="#241c15" stroke-width="5"/><circle cx="45" cy="43" r="4" fill="#d6bf98"/><circle cx="155" cy="43" r="4" fill="#d6bf98"/><rect x="44" y="26" width="112" height="104" rx="26" fill="#8b6643" stroke="#241c15" stroke-width="6"/><rect x="58" y="50" width="84" height="44" rx="12" fill="#0d0a07"/><circle cx="82" cy="72" r="8" fill="#e9b48a"/><circle cx="118" cy="72" r="8" fill="#e9b48a"/><rect x="78" y="98" width="44" height="22" rx="10" fill="#e8c9a0" stroke="#241c15" stroke-width="3"/><rect x="94" y="100" width="12" height="7" rx="3" fill="#241c15"/><rect x="93" y="116" width="14" height="9" rx="1" fill="#fbf8f1" stroke="#241c15" stroke-width="2"/></g></svg>
16
16
  <span class="brand-name">Groundhog</span>
17
17
  <span class="brand-from">from <a data-utm="header" href="https://groundmail.dev/?utm_source=groundhog&amp;utm_medium=app&amp;utm_campaign=header" target="_blank" rel="noopener">Groundmail</a></span>
18
18
  </div>
@@ -56,8 +56,7 @@
56
56
  <svg class="dialog-art" viewBox="0 0 720 150" aria-hidden="true">
57
57
  <rect x="0" y="112" width="720" height="10" fill="#4a3a29"/><rect x="0" y="122" width="720" height="10" fill="#3a2d20"/><rect x="0" y="132" width="720" height="18" fill="#2c2218"/>
58
58
  <ellipse cx="200" cy="114" rx="70" ry="12" fill="#0d0a07"/>
59
- <path d="M160 114 C160 84 176 70 200 70 C224 70 240 84 240 114 Z" fill="#8b5a33"/>
60
- <circle cx="190" cy="88" r="3.5" fill="#241c15"/><circle cx="210" cy="88" r="3.5" fill="#241c15"/><ellipse cx="200" cy="97" rx="4" ry="3" fill="#241c15"/>
59
+ <g transform="translate(150 30) scale(0.5)"><rect x="88" y="6" width="5" height="22" fill="#241c15"/><circle cx="90.5" cy="8" r="7" fill="#e27d4e" stroke="#241c15" stroke-width="3"/><rect x="34" y="30" width="22" height="26" rx="6" fill="#5f432c" stroke="#241c15" stroke-width="4"/><rect x="144" y="30" width="22" height="26" rx="6" fill="#5f432c" stroke="#241c15" stroke-width="4"/><circle cx="45" cy="43" r="3" fill="#d6bf98"/><circle cx="155" cy="43" r="3" fill="#d6bf98"/><rect x="44" y="26" width="112" height="104" rx="26" fill="#8b6643" stroke="#241c15" stroke-width="4"/><rect x="58" y="50" width="84" height="44" rx="12" fill="#0d0a07" stroke="#241c15" stroke-width="3"/><circle cx="82" cy="72" r="11" fill="#2a1a12"/><circle cx="118" cy="72" r="11" fill="#2a1a12"/><circle cx="86" cy="68" r="5.5" fill="#e9b48a"/><circle cx="122" cy="68" r="5.5" fill="#e9b48a"/><rect x="78" y="98" width="44" height="22" rx="10" fill="#e8c9a0" stroke="#241c15" stroke-width="3"/><rect x="94" y="100" width="12" height="7" rx="3" fill="#241c15"/><rect x="93" y="116" width="6" height="9" rx="1" fill="#fbf8f1" stroke="#241c15" stroke-width="2"/><rect x="101" y="116" width="6" height="9" rx="1" fill="#fbf8f1" stroke="#241c15" stroke-width="2"/><path d="M52 104h12M52 112h12M136 104h12M136 112h12" stroke="#5f432c" stroke-width="3" stroke-linecap="round"/><rect x="30" y="128" width="140" height="40" rx="8" fill="#241c15"/><rect x="84" y="136" width="32" height="22" rx="4" fill="#d6bf98"/><path d="M87 139 L100 149 L113 139" fill="none" stroke="#241c15" stroke-width="2.5" stroke-linejoin="round"/></g>
61
60
  <path d="M244 92 C330 40 420 40 500 70" fill="none" stroke="#7d6e5f" stroke-width="2" stroke-dasharray="4 6"/>
62
61
  <g transform="rotate(12 520 70)"><rect x="496" y="54" width="48" height="32" rx="4" fill="#fbf8f1"/><path d="M497 56 L520 72 L543 56" fill="none" stroke="#a3461f" stroke-width="2.5"/></g>
63
62
  <path d="M556 62 l14 -6 M558 74 l16 0 M556 86 l14 6" stroke="#e27d4e" stroke-width="2.5" stroke-linecap="round"/>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundmail/groundhog",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A local email inbox for development. Catches SMTP and the HTTP send APIs of Groundmail, SendGrid, Postmark, Mailgun, Resend and Amazon SES. Nothing leaves your machine.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -52,7 +52,7 @@
52
52
  "playwright"
53
53
  ],
54
54
  "scripts": {
55
- "build": "rm -rf dist && tsc -p . && cp -R src/ui dist/ui && chmod +x dist/cli.js",
55
+ "build": "node scripts/build.mjs clean && tsc -p . && node scripts/build.mjs ui",
56
56
  "dev": "npm run build && node dist/cli.js",
57
57
  "test": "vitest run",
58
58
  "test:e2e": "playwright test",