mirror of
https://github.com/jhillyerd/inbucket.git
synced 2026-01-28 14:05:57 +00:00
Compare commits
27 Commits
v3.1.0-bet
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69639ee20f | ||
|
|
be7d29c5d5 | ||
|
|
6eff554469 | ||
|
|
7413d06616 | ||
|
|
22c276ea1a | ||
|
|
dd22202aea | ||
|
|
32b0ff1ac6 | ||
|
|
e0824eb0aa | ||
|
|
f210b4c47c | ||
|
|
2ea0639509 | ||
|
|
4399d02f0b | ||
|
|
25007f4506 | ||
|
|
fe0e3a00e1 | ||
|
|
577a329240 | ||
|
|
c3a8eb8e3b | ||
|
|
273c6a5dbd | ||
|
|
f799e3debf | ||
|
|
8a1a01660c | ||
|
|
d1f2ae7946 | ||
|
|
8339cb5378 | ||
|
|
cf92969719 | ||
|
|
9a2b0f934a | ||
|
|
b99cf9b6dc | ||
|
|
f6d00dfcb2 | ||
|
|
440fddfe46 | ||
|
|
9904399d24 | ||
|
|
4c8c8e7744 |
24
.github/workflows/build-and-test.yml
vendored
24
.github/workflows/build-and-test.yml
vendored
@@ -9,19 +9,15 @@ on:
|
||||
jobs:
|
||||
linux-go-build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Linux Go ${{ matrix.go }} build
|
||||
strategy:
|
||||
matrix:
|
||||
go:
|
||||
- '1.23'
|
||||
name: Linux Go build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
go-version-file: '.go-version'
|
||||
check-latest: true
|
||||
- name: Build and test
|
||||
run: |
|
||||
@@ -31,20 +27,20 @@ jobs:
|
||||
uses: shogo82148/actions-goveralls@v1
|
||||
with:
|
||||
path-to-profile: profile.cov
|
||||
flag-name: Linux-Go-${{ matrix.go }}
|
||||
flag-name: Linux-Go
|
||||
parallel: true
|
||||
|
||||
windows-go-build:
|
||||
runs-on: windows-latest
|
||||
name: Windows Go build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.23'
|
||||
go-version-file: '.go-version'
|
||||
- name: Build
|
||||
run: go build ./...
|
||||
- name: Test
|
||||
@@ -60,11 +56,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: UI Build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'yarn'
|
||||
|
||||
2
.github/workflows/docker-build.yml
vendored
2
.github/workflows/docker-build.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
|
||||
8
.github/workflows/lint.yml
vendored
8
.github/workflows/lint.yml
vendored
@@ -7,11 +7,11 @@ jobs:
|
||||
golangci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.23'
|
||||
go-version-file: '.go-version'
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: latest
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -13,18 +13,18 @@ jobs:
|
||||
name: 'Go Releaser'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.23'
|
||||
go-version-file: '.go-version'
|
||||
check-latest: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'yarn'
|
||||
|
||||
1
.go-version
Normal file
1
.go-version
Normal file
@@ -0,0 +1 @@
|
||||
1.25
|
||||
@@ -1,5 +1,4 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- asasalint
|
||||
@@ -8,31 +7,19 @@ linters:
|
||||
- bodyclose
|
||||
- containedctx
|
||||
- contextcheck
|
||||
- copyloopvar
|
||||
- decorder
|
||||
# - dupl
|
||||
# - dupword
|
||||
- durationcheck
|
||||
- errchkjson
|
||||
- errname
|
||||
# - errorlint
|
||||
# - exhaustive
|
||||
- exportloopref
|
||||
# - forcetypeassert
|
||||
- ginkgolinter
|
||||
- gocheckcompilerdirectives
|
||||
# - gochecknoinits
|
||||
- gochecksumtype
|
||||
- gocritic
|
||||
# - godot
|
||||
# - goerr113
|
||||
- gofmt
|
||||
# - gofumpt
|
||||
- goheader
|
||||
- goimports
|
||||
- gomoddirectives
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
# - gosec
|
||||
- gosmopolitan
|
||||
- grouper
|
||||
- importas
|
||||
@@ -44,8 +31,6 @@ linters:
|
||||
- misspell
|
||||
- musttag
|
||||
- nilerr
|
||||
# - nilnil
|
||||
# - nlreturn
|
||||
- noctx
|
||||
- nolintlint
|
||||
- nosprintfhostport
|
||||
@@ -55,24 +40,43 @@ linters:
|
||||
- promlinter
|
||||
- protogetter
|
||||
- reassign
|
||||
# - revive
|
||||
- rowserrcheck
|
||||
- sloglint
|
||||
- stylecheck
|
||||
- staticcheck
|
||||
- tagliatelle
|
||||
- tenv
|
||||
- testableexamples
|
||||
- testifylint
|
||||
- thelper
|
||||
- tparallel
|
||||
# - unconvert
|
||||
- unparam
|
||||
- usestdlibvars
|
||||
- usetesting
|
||||
- wastedassign
|
||||
- whitespace
|
||||
- zerologlint
|
||||
linters-settings:
|
||||
tagliatelle:
|
||||
case:
|
||||
rules:
|
||||
json: kebab
|
||||
settings:
|
||||
tagliatelle:
|
||||
case:
|
||||
rules:
|
||||
json: kebab
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
@@ -27,6 +27,9 @@ builds:
|
||||
- arm64
|
||||
goarm:
|
||||
- "7"
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
main: ./cmd/inbucket
|
||||
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
|
||||
- id: inbucket-client
|
||||
@@ -44,16 +47,19 @@ builds:
|
||||
- arm64
|
||||
goarm:
|
||||
- "7"
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
main: ./cmd/client
|
||||
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
|
||||
|
||||
archives:
|
||||
- id: tarball
|
||||
format: tar.gz
|
||||
formats: tar.gz
|
||||
wrap_in_directory: true
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
formats: zip
|
||||
files:
|
||||
- LICENSE*
|
||||
- README*
|
||||
|
||||
293
AGENTS.md
Normal file
293
AGENTS.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# AGENTS.md - Inbucket
|
||||
|
||||
Guidance for AI agents working in this codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Inbucket is an email testing service that accepts messages for any email address and makes them available via web, REST, and POP3 interfaces. It's a self-contained Go application with an Elm-based web UI.
|
||||
|
||||
**Tech Stack:**
|
||||
- Backend: Go 1.25+
|
||||
- Frontend: Elm 0.19.1 with Parcel bundler
|
||||
- Logging: zerolog
|
||||
- Testing: testify (assert/require/suite), goldiff for golden file tests
|
||||
- HTTP Router: gorilla/mux
|
||||
- Configuration: envconfig (environment variables)
|
||||
- Optional: Lua scripting for extensions (gopher-lua)
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# Build Go binaries (inbucket server + client CLI)
|
||||
make build
|
||||
|
||||
# Or build directly
|
||||
go build ./cmd/inbucket
|
||||
go build ./cmd/client
|
||||
|
||||
# Build UI (required before running server)
|
||||
cd ui && yarn install && yarn build
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
# Run all Go tests with race detection
|
||||
make test
|
||||
# or
|
||||
go test -race ./...
|
||||
|
||||
# Run tests for a specific package
|
||||
go test -race ./pkg/storage/...
|
||||
|
||||
# Run tests with coverage
|
||||
go test -race -coverprofile=profile.cov ./...
|
||||
```
|
||||
|
||||
### Lint
|
||||
|
||||
```bash
|
||||
# CI uses golangci-lint
|
||||
golangci-lint run
|
||||
|
||||
# Make's lint target (older, uses golint)
|
||||
make lint
|
||||
```
|
||||
|
||||
### Run Development Server
|
||||
|
||||
```bash
|
||||
# Build everything first
|
||||
make build
|
||||
cd ui && yarn build && cd ..
|
||||
|
||||
# Run with dev config
|
||||
./etc/dev-start.sh
|
||||
|
||||
# Or run directly with defaults
|
||||
./inbucket
|
||||
```
|
||||
|
||||
Default ports:
|
||||
- Web UI: http://localhost:9000
|
||||
- SMTP: localhost:2500
|
||||
- POP3: localhost:1100
|
||||
|
||||
### UI Development
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
|
||||
# Install dependencies
|
||||
yarn install
|
||||
|
||||
# Development server with HMR (proxies to Go backend)
|
||||
yarn start
|
||||
|
||||
# Production build
|
||||
yarn build
|
||||
|
||||
# Clean build artifacts
|
||||
yarn clean
|
||||
```
|
||||
|
||||
## Code Organization
|
||||
|
||||
```
|
||||
cmd/
|
||||
inbucket/ # Main server binary
|
||||
client/ # CLI client for REST API
|
||||
|
||||
pkg/
|
||||
config/ # Environment-based configuration
|
||||
extension/ # Lua extension system
|
||||
luahost/ # Lua VM pool and bindings
|
||||
event/ # Extension event types
|
||||
message/ # Message manager (storage abstraction)
|
||||
metric/ # Expvar metrics
|
||||
msghub/ # Real-time message pub/sub
|
||||
policy/ # Email address/domain policies
|
||||
rest/ # REST API v1/v2 controllers
|
||||
client/ # Go client library for REST API
|
||||
model/ # JSON API models
|
||||
server/
|
||||
smtp/ # SMTP server
|
||||
pop3/ # POP3 server
|
||||
web/ # HTTP server, handlers, helpers
|
||||
storage/ # Storage interface and implementations
|
||||
file/ # File-based storage
|
||||
mem/ # In-memory storage
|
||||
stringutil/ # String utilities
|
||||
test/ # Test utilities and integration tests
|
||||
webui/ # Web UI controllers
|
||||
|
||||
ui/
|
||||
src/
|
||||
Main.elm # Elm app entry point
|
||||
Api.elm # API client
|
||||
Page/ # Page modules (Home, Mailbox, Monitor, Status)
|
||||
Data/ # Data models
|
||||
tests/ # Elm tests
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Inbucket uses environment variables for all configuration. Key variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `INBUCKET_LOGLEVEL` | `info` | debug, info, warn, error |
|
||||
| `INBUCKET_MAILBOXNAMING` | `local` | local, full, or domain |
|
||||
| `INBUCKET_SMTP_ADDR` | `0.0.0.0:2500` | SMTP listen address |
|
||||
| `INBUCKET_WEB_ADDR` | `0.0.0.0:9000` | HTTP listen address |
|
||||
| `INBUCKET_POP3_ADDR` | `0.0.0.0:1100` | POP3 listen address |
|
||||
| `INBUCKET_STORAGE_TYPE` | `memory` | `memory` or `file` |
|
||||
| `INBUCKET_WEB_UIDIR` | `ui/dist` | Path to built UI files |
|
||||
|
||||
Run `./inbucket -help` for complete list.
|
||||
|
||||
See `doc/config.md` for detailed documentation.
|
||||
|
||||
## Code Patterns
|
||||
|
||||
### Error Handling
|
||||
- Use zerolog for structured logging
|
||||
- Return errors up the call stack; log at the top level
|
||||
- Use `github.com/pkg/errors` patterns for wrapping
|
||||
|
||||
### HTTP Handlers
|
||||
Handlers follow this pattern in `pkg/server/web/`:
|
||||
```go
|
||||
func Handler(f func(http.ResponseWriter, *http.Request, *Context) error) http.Handler
|
||||
```
|
||||
|
||||
Controllers return errors; the wrapper handles HTTP responses.
|
||||
|
||||
### Storage Interface
|
||||
New storage backends implement `storage.Store` interface (`pkg/storage/storage.go`):
|
||||
```go
|
||||
type Store interface {
|
||||
AddMessage(message Message) (id string, err error)
|
||||
GetMessage(mailbox, id string) (Message, error)
|
||||
GetMessages(mailbox string) ([]Message, error)
|
||||
MarkSeen(mailbox, id string) error
|
||||
PurgeMessages(mailbox string) error
|
||||
RemoveMessage(mailbox, id string) error
|
||||
VisitMailboxes(f func([]Message) (cont bool)) error
|
||||
}
|
||||
```
|
||||
|
||||
Register in `cmd/inbucket/main.go` init():
|
||||
```go
|
||||
storage.Constructors["mytype"] = mystore.New
|
||||
```
|
||||
|
||||
### JSON Tag Convention
|
||||
JSON fields use kebab-case (configured in `.golangci.yml` tagliatelle):
|
||||
```go
|
||||
type Example struct {
|
||||
FieldName string `json:"field-name"`
|
||||
}
|
||||
```
|
||||
|
||||
### Elm Architecture
|
||||
The UI follows The Elm Architecture:
|
||||
- `Main.elm` - App shell, routing
|
||||
- `Page/*.elm` - Page modules with Model, Msg, init, update, view
|
||||
- `Data/*.elm` - Data types and JSON decoders
|
||||
- `Api.elm` - HTTP client for REST API
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Structure
|
||||
- Unit tests: alongside source files (`*_test.go`)
|
||||
- Integration tests: `pkg/test/integration_test.go`
|
||||
- Test utilities: `pkg/test/`
|
||||
|
||||
### Test Frameworks
|
||||
- Standard `testing` package
|
||||
- `github.com/stretchr/testify/assert` - assertions
|
||||
- `github.com/stretchr/testify/require` - fatal assertions
|
||||
- `github.com/stretchr/testify/suite` - test suites
|
||||
- `github.com/jhillyerd/goldiff` - golden file testing
|
||||
|
||||
### Test Utilities
|
||||
Located in `pkg/test/`:
|
||||
- `StoreStub`, `ManagerStub` - mock implementations
|
||||
- `DeliverToStore()` - create test messages
|
||||
- `StoreSuite()` - table-driven storage tests
|
||||
- `NewLuaState()` - Lua testing helper
|
||||
|
||||
### Golden File Tests
|
||||
Input in `pkg/test/testdata/*.txt`, expected output in `*.golden`:
|
||||
```go
|
||||
goldiff.File(t, got, "testdata", "basic.golden")
|
||||
```
|
||||
|
||||
### Running Specific Tests
|
||||
```bash
|
||||
# Run tests matching pattern
|
||||
go test -race -run TestIntegration ./pkg/test/
|
||||
|
||||
# Run with verbose output
|
||||
go test -race -v ./pkg/storage/mem/
|
||||
|
||||
# Run storage suite for specific implementation
|
||||
go test -race -run TestMemStore ./pkg/storage/mem/
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
GitHub Actions workflows in `.github/workflows/`:
|
||||
|
||||
- `build-and-test.yml` - Build and test on Linux/Windows, coverage to coveralls
|
||||
- `lint.yml` - golangci-lint
|
||||
- `docker-build.yml` - Docker image builds
|
||||
- `release.yml` - goreleaser for releases
|
||||
|
||||
## Important Gotchas
|
||||
|
||||
1. **UI must be built before running server** - The Go server serves static files from `ui/dist/`
|
||||
|
||||
2. **Storage type affects persistence** - `memory` storage loses all data on restart; use `file` for persistence
|
||||
|
||||
3. **Port conflicts** - Default ports (9000, 2500, 1100) may conflict with other services
|
||||
|
||||
4. **Lua scripting is optional** - If `inbucket.lua` is not present, the server runs without extensions
|
||||
|
||||
5. **Test coverage requires race detector** - CI always runs with `-race`
|
||||
|
||||
6. **golangci-lint v2 config** - Uses v2 format in `.golangci.yml`
|
||||
|
||||
7. **Windows paths in storage** - Use `$` instead of `:` in file storage paths (e.g., `D$/inbucket`)
|
||||
|
||||
## REST API
|
||||
|
||||
Base URL: `http://localhost:9000/api/`
|
||||
|
||||
### API v1 Endpoints
|
||||
- `GET /v1/mailbox/{name}` - List messages
|
||||
- `GET /v1/mailbox/{name}/{id}` - Get message
|
||||
- `PATCH /v1/mailbox/{name}/{id}` - Mark as seen
|
||||
- `DELETE /v1/mailbox/{name}` - Purge mailbox
|
||||
- `DELETE /v1/mailbox/{name}/{id}` - Delete message
|
||||
- `GET /v1/mailbox/{name}/{id}/source` - Get raw source
|
||||
|
||||
### API v2 Endpoints
|
||||
- `GET /v2/monitor/messages` - WebSocket for real-time messages
|
||||
|
||||
Go client available: `github.com/inbucket/inbucket/v3/pkg/rest/client`
|
||||
|
||||
## Development Tips
|
||||
|
||||
1. **Quick iteration** - Use `make reflex` for auto-rebuild on Go file changes
|
||||
|
||||
2. **UI development** - Run `yarn start` in `ui/` for HMR; it proxies API requests to the Go server
|
||||
|
||||
3. **Debug network** - Run with `-netdebug` flag to dump SMTP/POP3 traffic
|
||||
|
||||
4. **Test email sending** - Use swaks or the test scripts in `etc/swaks-tests/`
|
||||
|
||||
5. **Check configuration** - Run `./inbucket -help` to see all env vars and defaults
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -7,6 +7,22 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [v3.1.1] - 2025-12-06
|
||||
|
||||
### Fixed
|
||||
- Go version update for CVE-2025-47907
|
||||
- Removed broken Windows arm7 build (#589)
|
||||
|
||||
|
||||
## [v3.1.0] - 2025-07-27
|
||||
|
||||
### Added
|
||||
- Note in logs that a missing Lua script is not an error (#575)
|
||||
|
||||
### Fixed
|
||||
- Accept and handle emails sent with an empty 821.From (#561)
|
||||
|
||||
|
||||
## [v3.1.0-beta3] - 2024-11-02
|
||||
|
||||
### Added
|
||||
@@ -370,7 +386,9 @@ No change from beta1.
|
||||
specific message.
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/inbucket/inbucket/compare/v3.1.0-beta3...main
|
||||
[Unreleased]: https://github.com/inbucket/inbucket/compare/v3.1.1...main
|
||||
[v3.1.1]: https://github.com/inbucket/inbucket/compare/v3.1.0...v3.1.1
|
||||
[v3.1.0]: https://github.com/inbucket/inbucket/compare/v3.1.0-beta3...v3.1.0
|
||||
[v3.1.0-beta3]: https://github.com/inbucket/inbucket/compare/v3.1.0-beta2...v3.1.0-beta3
|
||||
[v3.1.0-beta2]: https://github.com/inbucket/inbucket/compare/v3.1.0-beta1...v3.1.0-beta2
|
||||
[v3.1.0-beta1]: https://github.com/inbucket/inbucket/compare/v3.0.4...v3.1.0-beta1
|
||||
@@ -409,7 +427,7 @@ No change from beta1.
|
||||
- Add new GitHub `/compare` link
|
||||
- Update previous tag version for *Unreleased*
|
||||
3. Run tests
|
||||
4. Update goreleaser, and then test cross-compile: `goreleaser --snapshot`
|
||||
4. Update goreleaser, and then test cross-compile: `goreleaser release --snapshot --clean`
|
||||
5. Commit changes and merge release PR into main
|
||||
6. Create new release via GitHub, use CHANGELOG release notes, tag `vX.Y.Z`
|
||||
7. Push tags and wait for
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
### Build frontend
|
||||
# Due to no official elm compiler for arm; build frontend with amd64.
|
||||
FROM --platform=linux/amd64 node:20 as frontend
|
||||
FROM --platform=linux/amd64 node:20 AS frontend
|
||||
RUN npm install -g node-gyp
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
@@ -12,7 +12,7 @@ RUN yarn install --frozen-lockfile --non-interactive
|
||||
RUN yarn run build
|
||||
|
||||
### Build backend
|
||||
FROM golang:1.23-alpine3.20 as backend
|
||||
FROM golang:1.25-alpine3.22 AS backend
|
||||
RUN apk add --no-cache --virtual .build-deps g++ git make
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
@@ -23,7 +23,7 @@ RUN go build -o inbucket \
|
||||
-v ./cmd/inbucket
|
||||
|
||||
### Run in minimal image
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.22
|
||||
RUN apk --no-cache add tzdata
|
||||
WORKDIR /opt/inbucket
|
||||
RUN mkdir bin defaults ui
|
||||
|
||||
@@ -30,6 +30,13 @@ Inbucket has automated [Docker Image] builds via Docker Hub. The `latest` tag
|
||||
tracks our tagged releases, and `edge` tracks our potentially unstable
|
||||
`main` branch.
|
||||
|
||||
Start the docker image by running:
|
||||
|
||||
```
|
||||
docker run -d --name inbucket -p 9000:9000 -p 2500:2500 -p 1100:1100 inbucket/inbucket
|
||||
```
|
||||
|
||||
Then point your browser to [localhost:9000](http://localhost:9000/)
|
||||
|
||||
## Building from Source
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ func init() {
|
||||
startTime.Set(time.Now().UnixNano() / 1000000)
|
||||
|
||||
// Goroutine count for status page.
|
||||
expvar.Publish("goroutines", expvar.Func(func() interface{} {
|
||||
expvar.Publish("goroutines", expvar.Func(func() any {
|
||||
return runtime.NumGoroutine()
|
||||
}))
|
||||
|
||||
|
||||
18
go.mod
18
go.mod
@@ -1,8 +1,6 @@
|
||||
module github.com/inbucket/inbucket/v3
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.4
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/cjoudrey/gluahttp v0.0.0-20201111170219-25003d9adfa9
|
||||
@@ -12,14 +10,14 @@ require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/inbucket/gopher-json v0.2.0
|
||||
github.com/jhillyerd/enmime/v2 v2.0.0
|
||||
github.com/jhillyerd/enmime/v2 v2.1.0
|
||||
github.com/jhillyerd/goldiff v0.1.0
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/yuin/gopher-lua v1.1.1
|
||||
golang.org/x/net v0.29.0
|
||||
golang.org/x/net v0.40.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -29,7 +27,7 @@ require (
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@@ -39,8 +37,8 @@ require (
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
29
go.sum
29
go.sum
@@ -27,8 +27,8 @@ github.com/inbucket/gopher-json v0.2.0 h1:v/luoFy5olitFhByVUGMZ3LmtcroRs9YHlyrBe
|
||||
github.com/inbucket/gopher-json v0.2.0/go.mod h1:1BK2XgU9y+ibiRkylJQeV44AV9DrO8dVsgOJ6vpqF3g=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jhillyerd/enmime/v2 v2.0.0 h1:I39PYf0peLGroKq+uX2yGB1ExH/78HcRJy4VmERQAVk=
|
||||
github.com/jhillyerd/enmime/v2 v2.0.0/go.mod h1:wQkz7BochDzSukAz5ajAQaXOB7pEg5Vh5QWs7m1uAPw=
|
||||
github.com/jhillyerd/enmime/v2 v2.1.0 h1:c8Qwi5Xq5EdtMN6byQWoZ/8I2RMTo6OJ7Xay+s1oPO0=
|
||||
github.com/jhillyerd/enmime/v2 v2.1.0/go.mod h1:EJ74dcRbBcqHSP2TBu08XRoy6y3Yx0cevwb1YkGMEmQ=
|
||||
github.com/jhillyerd/goldiff v0.1.0 h1:7JzKPKVwAg1GzrbnsToYzq3Y5+S7dXM4hgEYiOzaf4A=
|
||||
github.com/jhillyerd/goldiff v0.1.0/go.mod h1:WeDal6DTqhbMhNkf5REzWCIvKl3JWs0Q9omZ/huIWAs=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
@@ -40,8 +40,9 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@@ -65,26 +66,26 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 h1:noHsffKZsNfU38DwcXWEPldrTjIZ8FPNKx8mYMGnqjs=
|
||||
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7/go.mod h1:bbMEM6aU1WDF1ErA5YJ0p91652pGv140gGw4Ww3RGp8=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
@@ -38,7 +38,7 @@ func New(conf config.Lua, extHost *extension.Host) (*Host, error) {
|
||||
|
||||
// Pre-load, parse, and compile script.
|
||||
if fi, err := os.Stat(scriptPath); err != nil {
|
||||
logger.Info().Msg("Script file not found")
|
||||
logger.Info().Msg("Lua script file not found (this is not an error)")
|
||||
return nil, ErrNoScript
|
||||
} else if fi.IsDir() {
|
||||
return nil, fmt.Errorf("lua script %v is a directory", scriptPath)
|
||||
|
||||
@@ -112,7 +112,7 @@ func (s *StoreManager) Deliver(
|
||||
for _, mb := range inbound.Mailboxes {
|
||||
// Append recipient and timestamp to generated Received header.
|
||||
recvd := fmt.Sprintf("%s for <%s>; %s\r\n", recvdHeader, mb, tstamp)
|
||||
|
||||
returnPath := fmt.Sprintf("Return-Path: <%s>\r\n", from.Address.Address)
|
||||
// Deliver message.
|
||||
logger.Debug().Str("mailbox", mb).Msg("Delivering message")
|
||||
delivery := &Delivery{
|
||||
@@ -124,7 +124,7 @@ func (s *StoreManager) Deliver(
|
||||
Subject: inbound.Subject,
|
||||
Size: inbound.Size,
|
||||
},
|
||||
Reader: io.MultiReader(strings.NewReader(recvd), bytes.NewReader(source)),
|
||||
Reader: io.MultiReader(strings.NewReader(returnPath), strings.NewReader(recvd), bytes.NewReader(source)),
|
||||
}
|
||||
id, err := s.Store.AddMessage(delivery)
|
||||
if err != nil {
|
||||
|
||||
@@ -501,6 +501,38 @@ func TestMailboxForAddress(t *testing.T) {
|
||||
assert.Equal(t, addr, got, "FullNaming mode should return a full address for mailbox")
|
||||
}
|
||||
|
||||
func TestReturnPath(t *testing.T) {
|
||||
sm, _ := testStoreManager()
|
||||
|
||||
recvdHeader := "Received: xyz\n"
|
||||
msgSource := `From: from@example.com
|
||||
To: u1@example.com
|
||||
Subject: return path
|
||||
|
||||
test email`
|
||||
|
||||
// Deliver message.
|
||||
origin, _ := sm.AddrPolicy.ParseOrigin("821from@example.com")
|
||||
recipient, _ := sm.AddrPolicy.NewRecipient("u1@example.com")
|
||||
err := sm.Deliver(origin, []*policy.Recipient{recipient}, recvdHeader, []byte(msgSource))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find message ID.
|
||||
msgs, err := sm.GetMetadata("u1@example.com")
|
||||
require.NoError(t, err, "Failed to read mailbox")
|
||||
require.Len(t, msgs, 1, "Unexpected mailbox len")
|
||||
id := msgs[0].ID
|
||||
|
||||
// Read back and verify source.
|
||||
r, err := sm.SourceReader("u1@example.com", id)
|
||||
require.NoError(t, err, "SourceReader must succeed")
|
||||
gotBytes, err := io.ReadAll(r)
|
||||
require.NoError(t, err, "Failed to read source")
|
||||
|
||||
got := string(gotBytes)
|
||||
assert.Contains(t, got, "Return-Path: <821from@example.com>\r\n", "Source should contain return-path")
|
||||
}
|
||||
|
||||
// Returns an empty StoreManager and extension Host pair, configured for testing.
|
||||
func testStoreManager() (*message.StoreManager, *extension.Host) {
|
||||
extHost := extension.NewHost()
|
||||
|
||||
@@ -81,7 +81,7 @@ func TestHubZeroLen(t *testing.T) {
|
||||
hub := New(0, extension.NewHost())
|
||||
go hub.Start(ctx)
|
||||
m := event.MessageMetadata{}
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
hub.Dispatch(m)
|
||||
}
|
||||
// Ensures Hub doesn't panic
|
||||
@@ -93,7 +93,7 @@ func TestHubZeroListeners(t *testing.T) {
|
||||
hub := New(5, extension.NewHost())
|
||||
go hub.Start(ctx)
|
||||
m := event.MessageMetadata{}
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
hub.Dispatch(m)
|
||||
}
|
||||
// Ensures Hub doesn't panic
|
||||
@@ -178,7 +178,7 @@ func TestHubHistoryReplay(t *testing.T) {
|
||||
|
||||
// Broadcast 3 messages with no listeners
|
||||
msgs := make([]event.MessageMetadata, 3)
|
||||
for i := 0; i < len(msgs); i++ {
|
||||
for i := range msgs {
|
||||
msgs[i] = event.MessageMetadata{
|
||||
Subject: fmt.Sprintf("subj %v", i),
|
||||
}
|
||||
@@ -203,7 +203,7 @@ func TestHubHistoryReplay(t *testing.T) {
|
||||
t.Fatal("Timeout:", l2)
|
||||
}
|
||||
|
||||
for i := 0; i < len(msgs); i++ {
|
||||
for i := range msgs {
|
||||
got := l2.messages[i].Subject
|
||||
want := msgs[i].Subject
|
||||
if got != want {
|
||||
@@ -222,7 +222,7 @@ func TestHubHistoryDelete(t *testing.T) {
|
||||
|
||||
// Broadcast 3 messages with no listeners
|
||||
msgs := make([]event.MessageMetadata, 3)
|
||||
for i := 0; i < len(msgs); i++ {
|
||||
for i := range msgs {
|
||||
msgs[i] = event.MessageMetadata{
|
||||
Mailbox: "hub",
|
||||
ID: strconv.Itoa(i),
|
||||
@@ -253,7 +253,7 @@ func TestHubHistoryDelete(t *testing.T) {
|
||||
}
|
||||
|
||||
want := []string{"subj 0", "subj 2"}
|
||||
for i := 0; i < len(want); i++ {
|
||||
for i := range want {
|
||||
got := l2.messages[i].Subject
|
||||
if got != want[i] {
|
||||
t.Errorf("msg[%v].Subject == %q, want %q", i, got, want[i])
|
||||
@@ -271,7 +271,7 @@ func TestHubHistoryReplayWrap(t *testing.T) {
|
||||
|
||||
// Broadcast more messages than the hub can hold
|
||||
msgs := make([]event.MessageMetadata, 20)
|
||||
for i := 0; i < len(msgs); i++ {
|
||||
for i := range msgs {
|
||||
msgs[i] = event.MessageMetadata{
|
||||
Subject: fmt.Sprintf("subj %v", i),
|
||||
}
|
||||
@@ -296,7 +296,7 @@ func TestHubHistoryReplayWrap(t *testing.T) {
|
||||
t.Fatal("Timeout:", l2)
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
for i := range 5 {
|
||||
got := l2.messages[i].Subject
|
||||
want := msgs[i+15].Subject
|
||||
if got != want {
|
||||
@@ -326,7 +326,7 @@ func TestHubHistoryReplayWrapAfterDelete(t *testing.T) {
|
||||
|
||||
// Broadcast more messages than the hub can hold.
|
||||
msgs := make([]event.MessageMetadata, 10)
|
||||
for i := 0; i < len(msgs); i++ {
|
||||
for i := range msgs {
|
||||
msgs[i] = event.MessageMetadata{
|
||||
Mailbox: "first",
|
||||
ID: strconv.Itoa(i),
|
||||
@@ -343,7 +343,7 @@ func TestHubHistoryReplayWrapAfterDelete(t *testing.T) {
|
||||
hub.Delete("first", "7")
|
||||
|
||||
// Broadcast another set of messages.
|
||||
for i := 0; i < len(msgs); i++ {
|
||||
for i := range msgs {
|
||||
msgs[i] = event.MessageMetadata{
|
||||
Mailbox: "second",
|
||||
ID: strconv.Itoa(i),
|
||||
|
||||
@@ -75,6 +75,11 @@ func (a *Addressing) NewRecipient(address string) (*Recipient, error) {
|
||||
// ParseOrigin parses an address into a Origin. This is used for parsing MAIL FROM argument,
|
||||
// not To headers.
|
||||
func (a *Addressing) ParseOrigin(address string) (*Origin, error) {
|
||||
if address == "" {
|
||||
return &Origin{
|
||||
addrPolicy: a,
|
||||
}, nil
|
||||
}
|
||||
local, domain, err := ParseEmailAddress(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -269,7 +274,7 @@ func parseEmailAddress(address string) (local string, domain string, err error)
|
||||
inCharQuote := false
|
||||
inStringQuote := false
|
||||
LOOP:
|
||||
for i := 0; i < len(address); i++ {
|
||||
for i := range len(address) {
|
||||
c := address[i]
|
||||
switch {
|
||||
case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'):
|
||||
@@ -375,7 +380,7 @@ func parseMailboxName(localPart string) (result string, err error) {
|
||||
}
|
||||
result = strings.ToLower(localPart)
|
||||
invalid := make([]byte, 0, 10)
|
||||
for i := 0; i < len(result); i++ {
|
||||
for i := range len(result) {
|
||||
c := result[i]
|
||||
switch {
|
||||
case 'a' <= c && c <= 'z':
|
||||
|
||||
@@ -2,6 +2,7 @@ package pop3
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -101,7 +102,7 @@ func (s *Session) String() string {
|
||||
* 4. If bad cmd, respond error
|
||||
* 5. Goto 2
|
||||
*/
|
||||
func (s *Server) startSession(id int, conn net.Conn) {
|
||||
func (s *Server) startSession(ctx context.Context, id int, conn net.Conn) {
|
||||
logger := log.With().Str("module", "pop3").Str("remote", conn.RemoteAddr().String()).
|
||||
Int("session", id).Logger()
|
||||
logger.Debug().Msgf("ForceTLS: %t", s.config.ForceTLS)
|
||||
@@ -165,7 +166,7 @@ func (s *Server) startSession(id int, conn net.Conn) {
|
||||
// Send command to handler for current state
|
||||
switch ssn.state {
|
||||
case AUTHORIZATION:
|
||||
ssn.authorizationHandler(cmd, arg)
|
||||
ssn.authorizationHandler(ctx, cmd, arg)
|
||||
continue
|
||||
case TRANSACTION:
|
||||
ssn.transactionHandler(cmd, arg)
|
||||
@@ -206,7 +207,7 @@ func (s *Server) startSession(id int, conn net.Conn) {
|
||||
}
|
||||
|
||||
// AUTHORIZATION state
|
||||
func (s *Session) authorizationHandler(cmd string, args []string) {
|
||||
func (s *Session) authorizationHandler(ctx context.Context, cmd string, args []string) {
|
||||
switch cmd {
|
||||
case "QUIT":
|
||||
s.send("+OK Goodnight and good luck")
|
||||
@@ -214,7 +215,7 @@ func (s *Session) authorizationHandler(cmd string, args []string) {
|
||||
s.enterState(QUIT)
|
||||
|
||||
case "STLS":
|
||||
if !s.Server.config.TLSEnabled || s.Server.config.ForceTLS {
|
||||
if !s.config.TLSEnabled || s.config.ForceTLS {
|
||||
// Invalid command since TLS unconfigured.
|
||||
s.logger.Debug().Msgf("-ERR TLS unavailable on the server")
|
||||
s.send("-ERR TLS unavailable on the server")
|
||||
@@ -229,9 +230,11 @@ func (s *Session) authorizationHandler(cmd string, args []string) {
|
||||
s.logger.Debug().Msg("Initiating TLS context.")
|
||||
|
||||
// Start TLS connection handshake.
|
||||
tlsCtx, cancel := context.WithTimeout(ctx, s.config.Timeout)
|
||||
defer cancel()
|
||||
s.send("+OK Begin TLS Negotiation")
|
||||
tlsConn := tls.Server(s.conn, s.Server.tlsConfig)
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
tlsConn := tls.Server(s.conn, s.tlsConfig)
|
||||
if err := tlsConn.HandshakeContext(tlsCtx); err != nil {
|
||||
s.logger.Error().Msgf("-ERR TLS handshake failed %v", err)
|
||||
s.ooSeq(cmd)
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ func setupPOPServer(t *testing.T, ds storage.Store, tls bool, forceTLS bool) *Se
|
||||
cfg := config.POP3{
|
||||
Addr: "127.0.0.1:2500",
|
||||
Domain: "inbucket.local",
|
||||
Timeout: 5,
|
||||
Timeout: 5 * time.Second,
|
||||
Debug: true,
|
||||
ForceTLS: forceTLS,
|
||||
}
|
||||
@@ -344,7 +344,7 @@ func setupPOPSession(t *testing.T, server *Server) net.Conn {
|
||||
// Start the session.
|
||||
server.wg.Add(1)
|
||||
sessionNum++
|
||||
go server.startSession(sessionNum, &mockConn{serverConn})
|
||||
go server.startSession(context.Background(), sessionNum, &mockConn{serverConn})
|
||||
|
||||
return clientConn
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func (s *Server) serve(ctx context.Context) {
|
||||
} else {
|
||||
tempDelay = 0
|
||||
s.wg.Add(1)
|
||||
go s.startSession(sid, conn)
|
||||
go s.startSession(ctx, sid, conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ func (s *Session) greetHandler(cmd string, arg string) {
|
||||
s.send("250-" + readyBanner)
|
||||
s.send("250-8BITMIME")
|
||||
s.send("250-AUTH PLAIN LOGIN")
|
||||
if s.Server.config.TLSEnabled && !s.Server.config.ForceTLS && s.Server.tlsConfig != nil && s.tlsState == nil {
|
||||
if s.config.TLSEnabled && !s.config.ForceTLS && s.tlsConfig != nil && s.tlsState == nil {
|
||||
s.send("250-STARTTLS")
|
||||
}
|
||||
s.send(fmt.Sprintf("250 SIZE %v", s.config.MaxMessageBytes))
|
||||
@@ -338,7 +338,7 @@ func (s *Session) passwordHandler() {
|
||||
func (s *Session) readyHandler(cmd string, arg string) {
|
||||
switch cmd {
|
||||
case "STARTTLS":
|
||||
if !s.Server.config.TLSEnabled {
|
||||
if !s.config.TLSEnabled {
|
||||
// Invalid command since TLS unconfigured.
|
||||
s.logger.Debug().Msgf("454 TLS unavailable on the server")
|
||||
s.send("454 TLS unavailable on the server")
|
||||
@@ -354,7 +354,7 @@ func (s *Session) readyHandler(cmd string, arg string) {
|
||||
|
||||
// Start TLS connection handshake.
|
||||
s.send("220 STARTTLS")
|
||||
tlsConn := tls.Server(s.conn, s.Server.tlsConfig)
|
||||
tlsConn := tls.Server(s.conn, s.tlsConfig)
|
||||
s.conn = tlsConn
|
||||
s.text = textproto.NewConn(s.conn)
|
||||
s.tlsState = new(tls.ConnectionState)
|
||||
@@ -411,18 +411,6 @@ func (s *Session) parseMailFromCmd(arg string) {
|
||||
from := m[1]
|
||||
s.logger.Debug().Msgf("Mail sender is %v", from)
|
||||
|
||||
// Parse from address.
|
||||
_, domain, err := policy.ParseEmailAddress(from)
|
||||
s.logger.Debug().Msgf("Origin domain is %v", domain)
|
||||
if from != "" && err != nil {
|
||||
s.send("501 Bad sender address syntax")
|
||||
s.logger.Warn().Msgf("Bad address as MAIL arg: %q, %s", from, err)
|
||||
return
|
||||
}
|
||||
if from == "" {
|
||||
from = "unspecified"
|
||||
}
|
||||
|
||||
// Parse ESMTP parameters.
|
||||
if m[2] != "" {
|
||||
// Here the client may put BODY=8BITMIME, but Inbucket already
|
||||
@@ -480,7 +468,7 @@ func (s *Session) parseMailFromCmd(arg string) {
|
||||
// Ignore ShouldAccept if extensions explicitly allowed this From.
|
||||
if extAction == event.ActionDefer && !s.from.ShouldAccept() {
|
||||
s.send("501 Unauthorized domain")
|
||||
s.logger.Warn().Msgf("Bad domain sender %s", domain)
|
||||
s.logger.Warn().Msgf("Bad domain sender %s", origin.Domain)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,11 @@ func TestGreetState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Messages sent with a null reverse-path are unusual,
|
||||
// but valid. They are used for delivery status
|
||||
// notifications, and also for some sorts of auto-responder
|
||||
// as part of bounce storm mitigation.
|
||||
// Sections 3.6.3 and 4.5.5 of RFC 5321 discuss them.
|
||||
func TestEmptyEnvelope(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
server := setupSMTPServer(ds, extension.NewHost())
|
||||
@@ -85,14 +90,14 @@ func TestEmptyEnvelope(t *testing.T) {
|
||||
// Test out some empty envelope without blanks
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<>", 501},
|
||||
{"MAIL FROM:<>", 250},
|
||||
}
|
||||
playSession(t, server, script)
|
||||
|
||||
// Test out some empty envelope with blanks
|
||||
script = []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM: <>", 501},
|
||||
{"MAIL FROM: <>", 250},
|
||||
}
|
||||
playSession(t, server, script)
|
||||
}
|
||||
@@ -450,7 +455,6 @@ func TestBeforeMailFromAcceptedEventResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
for name, tc := range tcs {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Reset event listener.
|
||||
shouldReturn = &tc.eventRes
|
||||
@@ -619,7 +623,6 @@ func TestBeforeRcptToAcceptedEventResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
for name, tc := range tcs {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Reset event listener.
|
||||
shouldReturn = &tc.eventRes
|
||||
|
||||
@@ -130,6 +130,11 @@ func NewServer(conf *config.Root, mm message.Manager, mh *msghub.Hub) *Server {
|
||||
|
||||
// Start begins listening for HTTP requests
|
||||
func (s *Server) Start(ctx context.Context, readyFunc func()) {
|
||||
var (
|
||||
err error
|
||||
listenCfg net.ListenConfig
|
||||
)
|
||||
|
||||
server = &http.Server{
|
||||
Addr: rootConfig.Web.Addr,
|
||||
Handler: requestLoggingWrapper(Router),
|
||||
@@ -140,8 +145,11 @@ func (s *Server) Start(ctx context.Context, readyFunc func()) {
|
||||
// We don't use ListenAndServe because it lacks a way to close the listener
|
||||
log.Info().Str("module", "web").Str("phase", "startup").Str("addr", server.Addr).
|
||||
Msg("HTTP listening on tcp4")
|
||||
var err error
|
||||
listener, err = net.Listen("tcp", server.Addr)
|
||||
|
||||
// This context is only used while the listener is resolving our address.
|
||||
listenCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
listener, err = listenCfg.Listen(listenCtx, "tcp", server.Addr)
|
||||
if err != nil {
|
||||
log.Error().Str("module", "web").Str("phase", "startup").Err(err).
|
||||
Msg("HTTP failed to start TCP4 listener")
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestMaxSize(t *testing.T) {
|
||||
for _, mailbox := range boxes {
|
||||
go func(mailbox string) {
|
||||
size := int64(0)
|
||||
for i := 0; i < n; i++ {
|
||||
for range n {
|
||||
_, nbytes := test.DeliverToStore(t, s, mailbox, "subject", time.Now())
|
||||
size += nbytes
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func (m *ManagerStub) MarkSeen(mailbox, id string) error {
|
||||
}
|
||||
for _, msg := range m.mailboxes[mailbox] {
|
||||
if msg.ID == id {
|
||||
msg.MessageMetadata.Seen = true
|
||||
msg.Seen = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ func testMetadata(s storeSuite) {
|
||||
// testContent generates some binary content and makes sure it is correctly retrieved.
|
||||
func testContent(s storeSuite) {
|
||||
content := make([]byte, 5000)
|
||||
for i := 0; i < len(content); i++ {
|
||||
for i := range content {
|
||||
content[i] = byte(i % 256)
|
||||
}
|
||||
mailbox := "testmailbox"
|
||||
@@ -379,7 +379,7 @@ func testPurge(s storeSuite) {
|
||||
}
|
||||
gotEvents = append(gotEvents, ev)
|
||||
}
|
||||
assert.Equal(s, len(subjects), len(gotEvents),
|
||||
assert.Len(s, gotEvents, len(subjects),
|
||||
"expected delete event for each message in mailbox")
|
||||
}
|
||||
|
||||
@@ -388,7 +388,7 @@ func testMsgCap(s storeSuite) {
|
||||
mbCap := 10
|
||||
mailbox := "captain"
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
for i := range 20 {
|
||||
subj := fmt.Sprintf("subject %v", i)
|
||||
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
||||
msgs, err := s.store.GetMessages(mailbox)
|
||||
@@ -401,10 +401,7 @@ func testMsgCap(s storeSuite) {
|
||||
}
|
||||
|
||||
// Check that the first (oldest) message is correct.
|
||||
first := i - mbCap + 1
|
||||
if first < 0 {
|
||||
first = 0
|
||||
}
|
||||
first := max(i-mbCap+1, 0)
|
||||
firstSubj := fmt.Sprintf("subject %v", first)
|
||||
if firstSubj != msgs[0].Subject() {
|
||||
s.Errorf("Got subject %q, wanted first subject: %q", msgs[0].Subject(), firstSubj)
|
||||
@@ -415,7 +412,7 @@ func testMsgCap(s storeSuite) {
|
||||
// testNoMsgCap verfies a cap of 0 is not enforced.
|
||||
func testNoMsgCap(s storeSuite) {
|
||||
mailbox := "captain"
|
||||
for i := 0; i < 20; i++ {
|
||||
for i := range 20 {
|
||||
subj := fmt.Sprintf("subject %v", i)
|
||||
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
||||
GetAndCountMessages(s.T, s.store, mailbox, i+1)
|
||||
|
||||
@@ -31,7 +31,6 @@ func TestStoreStubMailboxAddGetVisit(t *testing.T) {
|
||||
{mailbox: "box3", count: 3},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
tc := tc
|
||||
t.Run(tc.mailbox, func(t *testing.T) {
|
||||
var err error
|
||||
|
||||
|
||||
2
pkg/test/testdata/basic.golden
vendored
2
pkg/test/testdata/basic.golden
vendored
@@ -2,7 +2,7 @@ Mailbox: recipient
|
||||
From: <fromuser@inbucket.org>
|
||||
To: [<recipient@inbucket.org>]
|
||||
Subject: basic subject
|
||||
Size: 204
|
||||
Size: 242
|
||||
|
||||
BODY TEXT:
|
||||
Basic message.
|
||||
|
||||
2
pkg/test/testdata/encodedheader.golden
vendored
2
pkg/test/testdata/encodedheader.golden
vendored
@@ -2,7 +2,7 @@ Mailbox: recipient
|
||||
From: X-äéß Y-äéß <fromuser@inbucket.org>
|
||||
To: [Test of ȇɲʢȯȡɪɴʛ <recipient@inbucket.org>]
|
||||
Subject: Test of ȇɲʢȯȡɪɴʛ
|
||||
Size: 338
|
||||
Size: 376
|
||||
|
||||
BODY TEXT:
|
||||
Basic message.
|
||||
|
||||
2
pkg/test/testdata/fullname.golden
vendored
2
pkg/test/testdata/fullname.golden
vendored
@@ -2,7 +2,7 @@ Mailbox: recipient
|
||||
From: From User <fromuser@inbucket.org>
|
||||
To: [Rec I. Pient <recipient@inbucket.org>]
|
||||
Subject: basic subject
|
||||
Size: 233
|
||||
Size: 271
|
||||
|
||||
BODY TEXT:
|
||||
Basic message.
|
||||
|
||||
2
pkg/test/testdata/no-to-ipv4.golden
vendored
2
pkg/test/testdata/no-to-ipv4.golden
vendored
@@ -2,7 +2,7 @@ Mailbox: ip4recipient
|
||||
From: <fromuser@inbucket.org>
|
||||
To: [<ip4recipient@[192.168.123.123]>]
|
||||
Subject: basic subject
|
||||
Size: 180
|
||||
Size: 218
|
||||
|
||||
BODY TEXT:
|
||||
No-To message.
|
||||
|
||||
2
pkg/test/testdata/no-to-ipv6.golden
vendored
2
pkg/test/testdata/no-to-ipv6.golden
vendored
@@ -2,7 +2,7 @@ Mailbox: ip6recipient
|
||||
From: <fromuser@inbucket.org>
|
||||
To: [<ip6recipient@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]>]
|
||||
Subject: basic subject
|
||||
Size: 180
|
||||
Size: 218
|
||||
|
||||
BODY TEXT:
|
||||
No-To message.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{ pkgs ? import <nixpkgs> { } }:
|
||||
{
|
||||
pkgs ? import <nixpkgs> { },
|
||||
}:
|
||||
let
|
||||
scripts = {
|
||||
# Quick test script.
|
||||
@@ -22,7 +24,7 @@ pkgs.mkShell {
|
||||
elmPackages.elm-json
|
||||
elmPackages.elm-language-server
|
||||
elmPackages.elm-test
|
||||
go_1_21
|
||||
go_1_25
|
||||
golangci-lint
|
||||
golint
|
||||
gopls
|
||||
|
||||
18
ui/yarn.lock
18
ui/yarn.lock
@@ -1016,9 +1016,9 @@ balanced-match@^1.0.0:
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
base-x@^3.0.8:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320"
|
||||
integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==
|
||||
version "3.0.11"
|
||||
resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.11.tgz#40d80e2a1aeacba29792ccc6c5354806421287ff"
|
||||
integrity sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
@@ -1578,9 +1578,9 @@ js-tokens@^4.0.0:
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
js-yaml@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
|
||||
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
@@ -1680,9 +1680,9 @@ lmdb@2.8.5:
|
||||
"@lmdb/lmdb-win32-x64" "2.8.5"
|
||||
|
||||
lodash@^4.17.19:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
version "4.17.23"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a"
|
||||
integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
|
||||
Reference in New Issue
Block a user