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
35 changes: 18 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,42 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions: write-all
permissions:
contents: read

jobs:
lint:
name: 🧹 Lint
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: 🔧 Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version-file: go.mod
cache: true

- name: 🕵️ Install golangci-lint
uses: golangci/golangci-lint-action@v9.2.0
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
args: --timeout=5m

- name: 🛡️ Run govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
GOTOOLCHAIN=go$(awk '/^go /{print $2; exit}' go.mod) go install golang.org/x/vuln/cmd/govulncheck@latest
GOTOOLCHAIN=go$(awk '/^go /{print $2; exit}' go.mod) govulncheck ./...

deps:
name: 📦 Dependencies
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: 🔧 Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version-file: go.mod
cache: true
Expand All @@ -64,10 +65,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: 🔧 Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version-file: go.mod
cache: true
Expand Down Expand Up @@ -96,10 +97,10 @@ jobs:
go: ${{ (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'release-run')) && fromJSON('["stable"]') || fromJSON('["stable", "oldstable"]') }}
steps:
- name: 📥 Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: 🔧 Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version: ${{ matrix.go }}
cache: true
Expand All @@ -117,10 +118,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: 🔧 Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version-file: go.mod
cache: true
Expand All @@ -129,7 +130,7 @@ jobs:
run: go test -covermode=atomic -coverpkg=./... -coverprofile=cover.out ./...

- name: ✅ Enforce coverage threshold
uses: vladopajic/go-test-coverage@v2
uses: vladopajic/go-test-coverage@a2cbf1fbcac20b3a5d09badb628331fa7bded52c # v2.14.0
with:
profile: cover.out
config: ./.github/.testcoverage.yml
Expand All @@ -149,10 +150,10 @@ jobs:
go: ${{ (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'release-run')) && fromJSON('["stable"]') || fromJSON('["stable", "oldstable"]') }}
steps:
- name: 📥 Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: 🔧 Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version: ${{ matrix.go }}
cache: true
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/cleanup-deployments.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Cleanup GitHub deployments
env:
Expand All @@ -44,6 +44,11 @@ jobs:
run: |
set -euo pipefail

if ! printf '%s' "${KEEP}" | grep -Eq '^[0-9]+$'; then
echo "keep_latest must be a non-negative integer"
exit 1
fi

# Ensure jq is available
if ! command -v jq >/dev/null 2>&1; then
sudo apt-get update -y
Expand Down Expand Up @@ -85,7 +90,7 @@ jobs:
echo "Found $total deployment(s)${ENV_FILTER:+ for environment '${ENV_FILTER}'}"

# Determine which to delete (skip the first KEEP entries)
to_delete=$(echo "$deployments_sorted" | jq ".[${KEEP}:]")
to_delete=$(echo "$deployments_sorted" | jq --argjson skip "$KEEP" '.[$skip:]')
del_count=$(echo "$to_delete" | jq 'length')
echo "Keeping ${KEEP}, candidate deletions: $del_count"
if [ "$del_count" -eq 0 ]; then
Expand Down
15 changes: 9 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ jobs:
goarch: 386
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version-file: go.mod
cache: true
Expand All @@ -42,7 +42,7 @@ jobs:
go build -trimpath -ldflags "-s -w" -o "dist/${BIN_NAME}-${{ matrix.goos }}-${{ matrix.goarch }}" ./

- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: inboundparse-${{ matrix.goos }}-${{ matrix.goarch }}
path: dist/*
Expand All @@ -52,17 +52,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: dist

- name: Assemble assets
run: |
mkdir -p assets
find dist -type f -maxdepth 2 -print -exec bash -lc 'f="{}"; base=$(basename "$f"); cp "$f" "assets/$base"' \;
find dist -type f -maxdepth 2 -print0 | while IFS= read -r -d '' f; do
base=$(basename "$f")
cp "$f" "assets/$base"
done

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
with:
files: assets/*
env:
Expand Down
37 changes: 20 additions & 17 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build stage
FROM golang:1.26.1-alpine AS builder
FROM golang:1.26.2-alpine3.21 AS builder

WORKDIR /app

Expand All @@ -16,27 +16,30 @@ COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o inboundparse ./cmd/inboundparse

# Final stage
FROM alpine:latest
FROM alpine:3.21

# Install ca-certificates, openssl, and curl for HTTPS webhook calls and certificate management
RUN apk --no-cache add ca-certificates openssl curl socat
ARG ACME_SH_VERSION=3.0.7

# Install acme.sh
RUN curl https://get.acme.sh | sh -s email=${ACME_SH_EMAIL:-dev@inboundparse.com}
# Install ca-certificates, openssl, socat, git, bash; pin acme.sh via git tag (no curl|sh)
RUN apk add --no-cache ca-certificates openssl socat git bash curl \
&& git clone --depth 1 --branch "${ACME_SH_VERSION}" https://github.com/acmesh-official/acme.sh.git /opt/acme.sh \
&& addgroup -S inboundparse \
&& adduser -S -G inboundparse -h /home/inboundparse inboundparse \
&& mkdir -p /cert /home/inboundparse \
&& chown -R inboundparse:inboundparse /cert /home/inboundparse /opt/acme.sh \
&& ln -sf /opt/acme.sh/acme.sh /usr/local/bin/acme.sh \
&& rm -rf /opt/acme.sh/.git

WORKDIR /root/
WORKDIR /home/inboundparse

# Copy the binary from builder stage
COPY --from=builder /app/inboundparse .
COPY --from=builder /app/inboundparse /usr/local/bin/inboundparse
COPY start.sh /usr/local/bin/start.sh

# Copy scripts
COPY start.sh ./start.sh
RUN chmod +x /usr/local/bin/inboundparse /usr/local/bin/start.sh \
&& chown inboundparse:inboundparse /usr/local/bin/inboundparse /usr/local/bin/start.sh

# Make scripts executable
RUN chmod +x ./start.sh

# Create cert directory
RUN mkdir -p /cert
USER inboundparse
ENV HOME=/home/inboundparse

# Environment variables for acme.sh and SMTP server
ENV CF_Token=""
Expand All @@ -61,4 +64,4 @@ EXPOSE 587
EXPOSE 9090

# Run the startup script
ENTRYPOINT ["./start.sh"]
ENTRYPOINT ["/usr/local/bin/start.sh"]
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
BINARY_NAME=inboundparse
DOCKER_IMAGE=inboundparse:latest
GOBIN := $(shell go env GOPATH)/bin
# govulncheck must load packages with the same Go version as go.mod; otherwise
# the loader can fail (e.g. internal error: x/sys/unix "without types").
GOTOOLCHAIN_GO := go$(shell awk '/^go /{print $$2; exit}' go.mod)

help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}'
Expand Down Expand Up @@ -68,9 +71,9 @@ lint: ## Lint (golangci-lint) + vuln scan (govulncheck)
@echo "Running golangci-lint..."
@command -v $(GOBIN)/golangci-lint >/dev/null 2>&1 || go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
@$(GOBIN)/golangci-lint run --fix --timeout=5m
@echo "Running govulncheck..."
@command -v $(GOBIN)/govulncheck >/dev/null 2>&1 || go install golang.org/x/vuln/cmd/govulncheck@latest
@$(GOBIN)/govulncheck ./...
@echo "Running govulncheck ($(GOTOOLCHAIN_GO))..."
@GOTOOLCHAIN=$(GOTOOLCHAIN_GO) go install golang.org/x/vuln/cmd/govulncheck@latest
@GOTOOLCHAIN=$(GOTOOLCHAIN_GO) $(GOBIN)/govulncheck ./...

fl: format lint vet ## Format, lint, and vet the code

Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ A SMTP server that receives emails from any domain without authentication and fo
> [!NOTE]
> This is my first major Golang product, so I may not do things the **golang way**, but I am open to feedback. I have used this application privately for the last 6 months, and have received well over **100,000** of pieces of mail (mostly spam, but thats for another day).


## Features

### Core SMTP Functionality
Expand All @@ -17,27 +16,31 @@ A SMTP server that receives emails from any domain without authentication and fo
- 📏 **Message Size Limits**: Configurable maximum message size (default: 10MB)

### Email Authentication (RFC Compliant)

- 🧐 **SPF Validation (RFC 7208)**: Validates Sender Policy Framework with HELO and envelope sender
- 🖊️ **DKIM Validation (RFC 6376)**: Verifies DomainKeys Identified Mail signatures with multi-signature support
- 🕵️‍♂️ **DMARC Validation (RFC 7489)**: Evaluates DMARC policy with hierarchical domain lookup and subdomain policy inheritance
- 📝 **Comprehensive Results**: Detailed authentication results with domain, mechanism, and alignment data
- 🔍 **Domain Hierarchy Tracking**: Tracks all attempted DMARC lookups with detailed per-domain results

### Observability & Monitoring

- 📋 **Structured Logging**: JSON-formatted logs with configurable levels
- 📊 **Prometheus Metrics**: Comprehensive metrics collection and monitoring
- 🛎️ **Sentry Integration**: Error tracking and performance monitoring
- ❤️ **Health Checks**: Built-in health endpoint for load balancers
- 📈 **Grafana Dashboards**: Pre-configured monitoring dashboards

### Deployment & Operations

- 🐳 **Docker Support**: Multi-stage Docker builds with Alpine Linux
- 🧩 **Docker Compose**: Complete development stack with monitoring
- 🚀 **Fly.io Ready**: Pre-configured for Fly.io deployment (more to come!)
- 🔑 **Automatic Certificates**: Let's Encrypt integration with DNS validation
- ⚙️ **Environment Configuration**: Flexible configuration via environment variables

### Webhook Integration

- 🔐 **HTTP Basic Auth**: Secure webhook authentication
- 📨 **Rich Payload**: Complete email data including headers and attachments
- 🔁 **Reliability Features**: Automatic retry with exponential backoff, rate limiting, and circuit breaker
Expand Down Expand Up @@ -102,6 +105,7 @@ make dev
```

With the dev stack running, you'll have:

- **InboundParse SMTP server** (`:25`, `:587`, `:9090`)
- **Prometheus** for metrics (`:9091`)
- **Grafana** dashboards (`:3000`, login `admin`/`admin`)
Expand Down Expand Up @@ -146,9 +150,7 @@ Usage of ./inboundparse:
-sentry-release string Sentry release version (optional)
```



### 🔒 Environment Variables
### 🔒 Environment Variables

You can configure InboundParse using environment variables or command line flags:

Expand Down Expand Up @@ -265,12 +267,12 @@ The service sends a JSON payload to your webhook with comprehensive email data:
}
```


## 🤝 Contributing

Contributions of all kinds are welcome! See [CONTRIBUTE.md](./docs/CONTRIBUTE.md) for guidelines, and don’t hesitate to open issues, fork, or create PRs.

---

> **Need help or want to suggest new features?**
> Open an issue or start a discussion on GitHub!
> Open an issue or start a discussion on GitHub!

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module inboundparse

go 1.26.1
go 1.26.2

require (
github.com/emersion/go-msgauth v0.7.0
Expand Down
Loading
Loading