Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Default reviewer for everything in this repo.
# See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
* @jack-arturo
65 changes: 65 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Bug report
description: Report a reproducible bug
labels: ["bug"]
body:
- type: textarea
id: what-happened
attributes:
label: What happened?
description: A clear description of the bug.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: Minimum steps that reliably trigger the bug.
placeholder: |
1. Run `npm run dev:local`
2. Open /login and seed sample data
3. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
validations:
required: false
- type: dropdown
id: area
attributes:
label: Area
options:
- Admin UI
- Contacts / import
- Campaigns / send
- Automations
- Ingest / integrations
- Tracking / unsubscribe
- Resend webhooks
- Deploy / Wrangler / D1
- Other
validations:
required: true
- type: input
id: node
attributes:
label: Node.js version
placeholder: e.g. v24.10.0
validations:
required: false
- type: input
id: os
attributes:
label: Operating system
placeholder: e.g. macOS 14.5, Ubuntu 24.04
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs / error output
render: shell
validations:
required: false
8 changes: 8 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Security vulnerability
url: https://github.com/verygoodplugins/emmail/security/advisories/new
about: Report vulnerabilities privately via GitHub Security Advisories.
- name: Very Good Plugins support
url: https://verygoodplugins.com/contact/?utm_source=github
about: Sales, billing, or non-bug questions.
31 changes: 31 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Feature request
description: Suggest a capability or improvement
labels: ["enhancement"]
body:
- type: textarea
id: problem
attributes:
label: Problem
description: What problem are you trying to solve? Who is affected?
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed solution
description: What should change? If you have a sketch or API in mind, share it.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Existing workarounds, related tools, or other approaches you ruled out.
validations:
required: false
- type: textarea
id: context
attributes:
label: Additional context
validations:
required: false
28 changes: 28 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!--
Title format: <type>[(<scope>)]: <summary> — scope is optional.
Examples: fix: harden campaign send idempotency · feat(automations): add draft preview
See https://www.conventionalcommits.org

Do not prefix titles with [codex], [claude], [copilot], [wip], or similar.
Put agent/status context in the PR body.
-->

## Summary

<!-- 1-3 sentences describing the change and why. -->

## Changes

-

## Test plan

- [ ] `npm run format:check`
- [ ] `npm run test:run`
- [ ] `npm run typecheck`
- [ ] Manual verification (describe below)
- [ ] Admin UI screenshot (if UI changed)

## Notes for reviewers

<!-- Risk areas: mail send, suppressions, secrets, migrations, config. -->
38 changes: 38 additions & 0 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Security Policy

## Reporting a Vulnerability

If you discover a security vulnerability in EmMail, please **do not** open a public issue.

Instead, report it privately via [GitHub Security Advisories](https://github.com/verygoodplugins/emmail/security/advisories/new). We aim to respond within 3 business days.

For urgent issues you may also email [support@verygoodplugins.com](mailto:support@verygoodplugins.com).

## Supported Versions

The latest commit on the default branch (`main`) receives security updates. Older forks and tags may be patched on a case-by-case basis.

## Disclosure Policy

We follow coordinated disclosure: we'll work with you on a fix and credit you in the release notes if you wish.

## Scope of particular interest

EmMail is a **single-tenant email marketing Worker** (contacts, broadcasts, automations, Resend webhooks, open/click/unsubscribe tracking). Reports of interest include:

- Admin auth bypass or cookie/session issues around `EMMAIL_ADMIN_TOKEN`
- Ingest endpoint abuse or secret leakage (`EMMAIL_INGEST_SECRET`)
- Tracking / unsubscribe token forgery (`TRACKING_SECRET`)
- Send-pipeline idempotency failures that could double-mail contacts
- Resend webhook signature bypass
- Secrets committed in `.dev.vars`, Wrangler config, or sample data

## Notes for self-hosters

If you fork this repository to deploy your own instance:

- **Create your own Cloudflare resources.** Example Worker / D1 / queue names and IDs in `wrangler.toml` are templates — replace them before a real deploy.
- **Set Worker secrets** (`wrangler secret put`): `RESEND_API_KEY`, `RESEND_WEBHOOK_SECRET`, `TRACKING_SECRET`, `EMMAIL_INGEST_SECRET`, `EMMAIL_ADMIN_TOKEN`. Use long random values.
- **Keep `EMMAIL_SEND_MODE=dry-run`** until Resend domain verification and the webhook loop are proven.
- **Never commit `.dev.vars` or `.env*`.** They are gitignored; keep them that way.
- **Rotate admin / ingest / tracking secrets** whenever someone with access leaves the project.
59 changes: 59 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
groups:
security-updates:
applies-to: security-updates
patterns:
- "*"
production-minor-patch:
applies-to: version-updates
dependency-type: "production"
update-types:
- "minor"
- "patch"
typescript-minor-patch:
applies-to: version-updates
dependency-type: "development"
patterns:
- "@types/*"
- "tsx"
- "typescript"
update-types:
- "minor"
- "patch"
test-tooling-minor-patch:
applies-to: version-updates
dependency-type: "development"
patterns:
- "@vitest/*"
- "vitest"
update-types:
- "minor"
- "patch"
development-minor-patch:
applies-to: version-updates
dependency-type: "development"
exclude-patterns:
- "@types/*"
- "@vitest/*"
- "tsx"
- "typescript"
- "vitest"
update-types:
- "minor"
- "patch"
commit-message:
prefix: "chore(deps)"
open-pull-requests-limit: 10

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore(deps)"
open-pull-requests-limit: 5
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]
merge_group:

permissions:
contents: read

jobs:
test:
name: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "24"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Format
run: npm run format:check

- name: Test
run: npm run test:run

- name: Build
run: npm run build
36 changes: 36 additions & 0 deletions .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Dependabot Auto-Merge

on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
workflow_run:
workflows: ["CI"]
types: [completed]

permissions:
contents: write
issues: write
pull-requests: write

jobs:
auto-merge:
if: >-
${{
(
github.event_name == 'pull_request_target' &&
github.event.pull_request.user.login == 'dependabot[bot]' &&
!github.event.pull_request.draft
) ||
(
github.event_name == 'workflow_run' &&
(
github.event.workflow_run.event == 'pull_request' ||
github.event.workflow_run.event == 'pull_request_target'
) &&
github.event.workflow_run.conclusion == 'success'
)
}}
# Pin to immutable SHA (not @main) so privileged write access cannot track a floating branch.
uses: verygoodplugins/.github/.github/workflows/dependabot-auto-merge.yml@20252be624eca94c9eb852c8ba1d1063ec734101 # main @ 2026-05-17
secrets:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
43 changes: 43 additions & 0 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: PR Title

on:
pull_request_target:
types:
- opened
- edited
- reopened
- synchronize

permissions:
pull-requests: read

jobs:
conventional-pr-title:
name: Lint PR Title
if: ${{ github.event.pull_request.base.ref == 'main' }}
runs-on: ubuntu-latest
steps:
- name: Validate conventional PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
# Reject agent/status prefixes that squash-merge into main history.
if [[ "$PR_TITLE" =~ ^\[ ]]; then
echo "PR titles must not start with bracketed agent/status prefixes like [codex], [claude], [copilot], or [wip]."
echo "Use a Conventional Commit title and put agent/status context in the PR body."
exit 1
fi

pattern='^(feat|fix|docs|chore|refactor|test|perf|build|ci|revert)(\([[:alnum:]_.-]+\))?!?: .+'

if [[ "$PR_TITLE" =~ $pattern ]]; then
echo "PR title is valid: $PR_TITLE"
exit 0
fi

echo "PR title must use Conventional Commits because squash merges become the commit title on main."
echo "Allowed prefixes: feat, fix, docs, chore, refactor, test, perf, build, ci, revert"
echo "Examples:"
echo " fix: harden campaign send idempotency"
echo " feat(automations): add draft preview"
exit 1
Loading