mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
Compare commits
84 Commits
v3.1.0-bet
...
v3.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
bd51662ce8 | ||
|
|
7d396a6bff | ||
|
|
b91a681ac0 | ||
|
|
9471035a59 | ||
|
|
5902189187 | ||
|
|
15d1970dbe | ||
|
|
78d4c4f4e7 | ||
|
|
9f90a59bef | ||
|
|
3110183a17 | ||
|
|
8097b3cc8a | ||
|
|
35549d9bf1 | ||
|
|
6a679bcbc0 | ||
|
|
81bc7c2ea7 | ||
|
|
f140cf7989 | ||
|
|
5284171dc5 | ||
|
|
b1b7e4b07c | ||
|
|
cdff6ea571 | ||
|
|
95ec463f26 | ||
|
|
924fb46b4e | ||
|
|
504a79aef4 | ||
|
|
543c2afda5 | ||
|
|
daeba2d024 | ||
|
|
b16764a65d | ||
|
|
0df07fc1be | ||
|
|
9478098c0f | ||
|
|
658506bb11 | ||
|
|
8826b8342b | ||
|
|
ffb4ce0b1b | ||
|
|
2b174c8b0b | ||
|
|
5729a212ce | ||
|
|
ed4a83a2bd | ||
|
|
c59e793775 | ||
|
|
40ec108daf | ||
|
|
185018e001 | ||
|
|
d62a0fede9 | ||
|
|
25c6f58535 | ||
|
|
e4ca20e471 | ||
|
|
7fa6b38b38 | ||
|
|
13ac9a6a1c | ||
|
|
0a51641a30 | ||
|
|
73203c6bcd | ||
|
|
6066be831c | ||
|
|
33784cbb94 | ||
|
|
f76b93a8f2 | ||
|
|
0361e971e0 | ||
|
|
def3e88651 | ||
|
|
8adae023dc | ||
|
|
fc8ea530bb | ||
|
|
ea585c4851 | ||
|
|
baa2dbd3a1 | ||
|
|
864a2ba2d2 | ||
|
|
b586ebe210 | ||
|
|
bed49706fc | ||
|
|
6ce1fd6347 | ||
|
|
3c0f253820 | ||
|
|
b2a77ad522 | ||
|
|
32cc4fc56d | ||
|
|
3112deb3e6 | ||
|
|
975cb4ca5e |
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
43
.github/workflows/build-and-test.yml
vendored
43
.github/workflows/build-and-test.yml
vendored
@@ -10,28 +10,23 @@ jobs:
|
||||
linux-go-build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Linux Go ${{ matrix.go }} build
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
go:
|
||||
- '1.21'
|
||||
|
||||
- '1.25'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
check-latest: true
|
||||
|
||||
- name: Build and test
|
||||
run: |
|
||||
go build ./...
|
||||
go test -race -coverprofile=profile.cov ./...
|
||||
|
||||
- name: Send coverage
|
||||
uses: shogo82148/actions-goveralls@v1
|
||||
with:
|
||||
@@ -42,23 +37,18 @@ jobs:
|
||||
windows-go-build:
|
||||
runs-on: windows-latest
|
||||
name: Windows Go build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
go-version: '1.25'
|
||||
- name: Build
|
||||
run: go build ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -race -coverprofile="profile.cov" ./...
|
||||
|
||||
- name: Send coverage
|
||||
uses: shogo82148/actions-goveralls@v1
|
||||
with:
|
||||
@@ -66,13 +56,32 @@ jobs:
|
||||
flag-name: Windows-Go
|
||||
parallel: true
|
||||
|
||||
ui-build:
|
||||
runs-on: ubuntu-latest
|
||||
name: UI Build
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: ui/yarn.lock
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
yarn install --frozen-lockfile --non-interactive
|
||||
yarn run build
|
||||
working-directory: ./ui
|
||||
|
||||
coverage:
|
||||
needs:
|
||||
- linux-go-build
|
||||
- windows-go-build
|
||||
name: Test Coverage
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: shogo82148/actions-goveralls@v1
|
||||
with:
|
||||
|
||||
14
.github/workflows/docker-build.yml
vendored
14
.github/workflows/docker-build.yml
vendored
@@ -20,11 +20,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
inbucket/inbucket
|
||||
@@ -39,30 +39,30 @@ jobs:
|
||||
latest=auto
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: ${{ env.REGISTRY_PUSH == 'true' }}
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ env.REGISTRY_PUSH == 'true' }}
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64, linux/arm/v7
|
||||
|
||||
14
.github/workflows/lint.yml
vendored
14
.github/workflows/lint.yml
vendored
@@ -2,22 +2,16 @@ name: Lint Go Code
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: '1.25'
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: latest
|
||||
# Disable cache to prevent `File exists` errors.
|
||||
# https://github.com/golangci/golangci-lint-action/issues/135
|
||||
skip-pkg-cache: true
|
||||
|
||||
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -6,25 +6,25 @@ on:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: 'Go Releaser'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'yarn'
|
||||
@@ -37,17 +37,19 @@ jobs:
|
||||
working-directory: ./ui
|
||||
|
||||
- name: Test build release
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||
with:
|
||||
version: latest
|
||||
distribution: goreleaser
|
||||
version: "~> v2"
|
||||
args: release --snapshot
|
||||
|
||||
- name: Build and publish release
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
if: "startsWith(github.ref, 'refs/tags/v')"
|
||||
with:
|
||||
version: latest
|
||||
distribution: goreleaser
|
||||
version: "~> v2"
|
||||
args: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
82
.golangci.yml
Normal file
82
.golangci.yml
Normal file
@@ -0,0 +1,82 @@
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- containedctx
|
||||
- contextcheck
|
||||
- copyloopvar
|
||||
- decorder
|
||||
- durationcheck
|
||||
- errchkjson
|
||||
- errname
|
||||
- ginkgolinter
|
||||
- gocheckcompilerdirectives
|
||||
- gochecksumtype
|
||||
- gocritic
|
||||
- goheader
|
||||
- gomoddirectives
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosmopolitan
|
||||
- grouper
|
||||
- importas
|
||||
- inamedparam
|
||||
- interfacebloat
|
||||
- loggercheck
|
||||
- makezero
|
||||
- mirror
|
||||
- misspell
|
||||
- musttag
|
||||
- nilerr
|
||||
- noctx
|
||||
- nolintlint
|
||||
- nosprintfhostport
|
||||
- perfsprint
|
||||
- prealloc
|
||||
- predeclared
|
||||
- promlinter
|
||||
- protogetter
|
||||
- reassign
|
||||
- rowserrcheck
|
||||
- sloglint
|
||||
- staticcheck
|
||||
- tagliatelle
|
||||
- testableexamples
|
||||
- testifylint
|
||||
- thelper
|
||||
- tparallel
|
||||
- unparam
|
||||
- usestdlibvars
|
||||
- usetesting
|
||||
- wastedassign
|
||||
- whitespace
|
||||
- zerologlint
|
||||
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$
|
||||
@@ -1,3 +1,4 @@
|
||||
version: 2 # goreleaser version
|
||||
project_name: inbucket
|
||||
|
||||
release:
|
||||
@@ -26,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
|
||||
@@ -43,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*
|
||||
|
||||
9
.luarc.json
Normal file
9
.luarc.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"runtime.version": "Lua 5.1",
|
||||
"diagnostics": {
|
||||
"globals": [
|
||||
"inbucket",
|
||||
"smtp"
|
||||
]
|
||||
}
|
||||
}
|
||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -4,6 +4,42 @@ Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
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
|
||||
- Lua scripting additions:
|
||||
- Add `SMTPSession` and `BeforeRcptToAccepted` event (#541)
|
||||
- Add `SMTPResponse` type for extensions (#539)
|
||||
- Add `RemoteAddr` to `SMTPSession` (#548)
|
||||
- Context support for REST client (#496)
|
||||
|
||||
### Fixed
|
||||
- Rename Lua `BeforeMailAccepted`, change args (#547)
|
||||
- pop3: Prevent STLS cmd triggered crashes (#516)
|
||||
- ui: date-format version, fixes yarn build (#508)
|
||||
- rework client example to omit `log.Fatal`, breaks defer (#489)
|
||||
- Rest Client: Allow relative URLs (#477)
|
||||
|
||||
|
||||
## [v3.1.0-beta2] - 2024-02-05
|
||||
|
||||
### Added
|
||||
@@ -350,7 +386,10 @@ No change from beta1.
|
||||
specific message.
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/inbucket/inbucket/compare/v3.1.0-beta2...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
|
||||
[v3.0.4]: https://github.com/inbucket/inbucket/compare/v3.0.3...v3.0.4
|
||||
@@ -388,10 +427,11 @@ 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`
|
||||
5. Commit changes and merge release into main, tag `vX.Y.Z`
|
||||
6. Push tags and wait for
|
||||
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
|
||||
[GitHub actions](https://github.com/inbucket/inbucket/actions) to complete
|
||||
7. Update `binary_versions` option in `inbucket-site/_config.yml`
|
||||
-- it will add compiled release assets
|
||||
|
||||
See http://keepachangelog.com/ for additional instructions on how to update this file.
|
||||
|
||||
28
Dockerfile
28
Dockerfile
@@ -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,18 +12,18 @@ RUN yarn install --frozen-lockfile --non-interactive
|
||||
RUN yarn run build
|
||||
|
||||
### Build backend
|
||||
FROM golang:1.21-alpine3.19 as backend
|
||||
FROM golang:1.25-alpine3.22 AS backend
|
||||
RUN apk add --no-cache --virtual .build-deps g++ git make
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
ENV CGO_ENABLED 0
|
||||
ENV CGO_ENABLED=0
|
||||
RUN make clean deps
|
||||
RUN go build -o inbucket \
|
||||
-ldflags "-X 'main.version=$(git describe --tags --always)' -X 'main.date=$(date -Iseconds)'" \
|
||||
-v ./cmd/inbucket
|
||||
|
||||
### Run in minimal image
|
||||
FROM alpine:3.19
|
||||
FROM alpine:3.22
|
||||
RUN apk --no-cache add tzdata
|
||||
WORKDIR /opt/inbucket
|
||||
RUN mkdir bin defaults ui
|
||||
@@ -33,16 +33,16 @@ COPY etc/docker/defaults/greeting.html defaults
|
||||
COPY etc/docker/defaults/start-inbucket.sh /
|
||||
|
||||
# Configuration
|
||||
ENV INBUCKET_SMTP_DISCARDDOMAINS bitbucket.local
|
||||
ENV INBUCKET_SMTP_TIMEOUT 30s
|
||||
ENV INBUCKET_POP3_TIMEOUT 30s
|
||||
ENV INBUCKET_WEB_GREETINGFILE /config/greeting.html
|
||||
ENV INBUCKET_WEB_COOKIEAUTHKEY secret-inbucket-session-cookie-key
|
||||
ENV INBUCKET_WEB_UIDIR ui
|
||||
ENV INBUCKET_STORAGE_TYPE file
|
||||
ENV INBUCKET_STORAGE_PARAMS path:/storage
|
||||
ENV INBUCKET_STORAGE_RETENTIONPERIOD 72h
|
||||
ENV INBUCKET_STORAGE_MAILBOXMSGCAP 300
|
||||
ENV INBUCKET_SMTP_DISCARDDOMAINS=bitbucket.local
|
||||
ENV INBUCKET_SMTP_TIMEOUT=30s
|
||||
ENV INBUCKET_POP3_TIMEOUT=30s
|
||||
ENV INBUCKET_WEB_GREETINGFILE=/config/greeting.html
|
||||
ENV INBUCKET_WEB_COOKIEAUTHKEY=secret-inbucket-session-cookie-key
|
||||
ENV INBUCKET_WEB_UIDIR=ui
|
||||
ENV INBUCKET_STORAGE_TYPE=file
|
||||
ENV INBUCKET_STORAGE_PARAMS=path:/storage
|
||||
ENV INBUCKET_STORAGE_RETENTIONPERIOD=72h
|
||||
ENV INBUCKET_STORAGE_MAILBOXMSGCAP=300
|
||||
|
||||
# Healthcheck
|
||||
HEALTHCHECK --interval=5s --timeout=5s --retries=3 CMD /bin/sh -c 'wget localhost:$(echo ${INBUCKET_WEB_ADDR:-0.0.0.0:9000}|cut -d: -f2) -q -O - >/dev/null'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -28,18 +28,20 @@ func (*listCmd) Usage() string {
|
||||
func (l *listCmd) SetFlags(f *flag.FlagSet) {}
|
||||
|
||||
func (l *listCmd) Execute(
|
||||
_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
mailbox := f.Arg(0)
|
||||
if mailbox == "" {
|
||||
return usage("mailbox required")
|
||||
}
|
||||
|
||||
// Setup rest client
|
||||
c, err := client.New(baseURL())
|
||||
if err != nil {
|
||||
return fatal("Couldn't build client", err)
|
||||
}
|
||||
|
||||
// Get list
|
||||
headers, err := c.ListMailbox(mailbox)
|
||||
headers, err := c.ListMailboxWithContext(ctx, mailbox)
|
||||
if err != nil {
|
||||
return fatal("REST call failed", err)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/subcommands"
|
||||
)
|
||||
@@ -68,7 +70,7 @@ func main() {
|
||||
}
|
||||
|
||||
func baseURL() string {
|
||||
return fmt.Sprintf("http://%s:%v", *host, *port)
|
||||
return "http://%s" + net.JoinHostPort(*host, strconv.FormatUint(uint64(*port), 10))
|
||||
}
|
||||
|
||||
func fatal(msg string, err error) subcommands.ExitStatus {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
type matchCmd struct {
|
||||
output string
|
||||
outFunc func(headers []*client.MessageHeader) error
|
||||
outFunc func(ctx context.Context, headers []*client.MessageHeader) error
|
||||
delete bool
|
||||
// match criteria
|
||||
from regexFlag
|
||||
@@ -51,11 +51,12 @@ func (m *matchCmd) SetFlags(f *flag.FlagSet) {
|
||||
}
|
||||
|
||||
func (m *matchCmd) Execute(
|
||||
_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
mailbox := f.Arg(0)
|
||||
if mailbox == "" {
|
||||
return usage("mailbox required")
|
||||
}
|
||||
|
||||
// Select output function
|
||||
switch m.output {
|
||||
case "id":
|
||||
@@ -67,16 +68,19 @@ func (m *matchCmd) Execute(
|
||||
default:
|
||||
return usage("unknown output type: " + m.output)
|
||||
}
|
||||
|
||||
// Setup REST client
|
||||
c, err := client.New(baseURL())
|
||||
if err != nil {
|
||||
return fatal("Couldn't build client", err)
|
||||
}
|
||||
|
||||
// Get list
|
||||
headers, err := c.ListMailbox(mailbox)
|
||||
headers, err := c.ListMailboxWithContext(ctx, mailbox)
|
||||
if err != nil {
|
||||
return fatal("List REST call failed", err)
|
||||
}
|
||||
|
||||
// Find matches
|
||||
matches := make([]*client.MessageHeader, 0, len(headers))
|
||||
for _, h := range headers {
|
||||
@@ -84,24 +88,28 @@ func (m *matchCmd) Execute(
|
||||
matches = append(matches, h)
|
||||
}
|
||||
}
|
||||
|
||||
// Return error status if no matches
|
||||
if len(matches) == 0 {
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
// Output matches
|
||||
err = m.outFunc(matches)
|
||||
err = m.outFunc(ctx, matches)
|
||||
if err != nil {
|
||||
return fatal("Error", err)
|
||||
}
|
||||
|
||||
// Optionally, delete matches
|
||||
if m.delete {
|
||||
// Delete matches
|
||||
for _, h := range matches {
|
||||
err = h.Delete()
|
||||
err = h.DeleteWithContext(ctx)
|
||||
if err != nil {
|
||||
return fatal("Delete REST call failed", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
@@ -148,14 +156,14 @@ func (m *matchCmd) match(header *client.MessageHeader) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func outputID(headers []*client.MessageHeader) error {
|
||||
func outputID(_ context.Context, headers []*client.MessageHeader) error {
|
||||
for _, h := range headers {
|
||||
fmt.Println(h.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputJSON(headers []*client.MessageHeader) error {
|
||||
func outputJSON(_ context.Context, headers []*client.MessageHeader) error {
|
||||
jsonEncoder := json.NewEncoder(os.Stdout)
|
||||
jsonEncoder.SetEscapeHTML(false)
|
||||
jsonEncoder.SetIndent("", " ")
|
||||
|
||||
@@ -33,44 +33,48 @@ func (m *mboxCmd) SetFlags(f *flag.FlagSet) {
|
||||
}
|
||||
|
||||
func (m *mboxCmd) Execute(
|
||||
_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
mailbox := f.Arg(0)
|
||||
if mailbox == "" {
|
||||
return usage("mailbox required")
|
||||
}
|
||||
|
||||
// Setup REST client
|
||||
c, err := client.New(baseURL())
|
||||
if err != nil {
|
||||
return fatal("Couldn't build client", err)
|
||||
}
|
||||
|
||||
// Get list
|
||||
headers, err := c.ListMailbox(mailbox)
|
||||
headers, err := c.ListMailboxWithContext(ctx, mailbox)
|
||||
if err != nil {
|
||||
return fatal("List REST call failed", err)
|
||||
}
|
||||
err = outputMbox(headers)
|
||||
err = outputMbox(ctx, headers)
|
||||
if err != nil {
|
||||
return fatal("Error", err)
|
||||
}
|
||||
|
||||
// Optionally, delete retrieved messages
|
||||
if m.delete {
|
||||
// Delete matches
|
||||
for _, h := range headers {
|
||||
err = h.Delete()
|
||||
err = h.DeleteWithContext(ctx)
|
||||
if err != nil {
|
||||
return fatal("Delete REST call failed", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
// outputMbox renders messages in mbox format
|
||||
// also used by match subcommand
|
||||
func outputMbox(headers []*client.MessageHeader) error {
|
||||
// outputMbox renders messages in mbox format.
|
||||
// It is also used by match subcommand.
|
||||
func outputMbox(ctx context.Context, headers []*client.MessageHeader) error {
|
||||
for _, h := range headers {
|
||||
source, err := h.GetSource()
|
||||
source, err := h.GetSourceWithContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Get source REST failed: %v", err)
|
||||
return fmt.Errorf("get source REST failed: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("From %s\n", h.From)
|
||||
|
||||
@@ -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()
|
||||
}))
|
||||
|
||||
@@ -162,7 +162,7 @@ signalLoop:
|
||||
}
|
||||
|
||||
// openLog configures zerolog output, returns func to close logfile.
|
||||
func openLog(level string, logfile string, json bool) (close func(), err error) {
|
||||
func openLog(level string, logfile string, json bool) (closeLog func(), err error) {
|
||||
switch level {
|
||||
case "debug":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
@@ -173,9 +173,10 @@ func openLog(level string, logfile string, json bool) (close func(), err error)
|
||||
case "error":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
default:
|
||||
return nil, fmt.Errorf("Log level %q not one of: debug, info, warn, error", level)
|
||||
return nil, fmt.Errorf("log level %q not one of: debug, info, warn, error", level)
|
||||
}
|
||||
close = func() {}
|
||||
|
||||
closeLog = func() {}
|
||||
var w io.Writer
|
||||
color := runtime.GOOS != "windows"
|
||||
switch logfile {
|
||||
@@ -191,21 +192,24 @@ func openLog(level string, logfile string, json bool) (close func(), err error)
|
||||
bw := bufio.NewWriter(logf)
|
||||
w = bw
|
||||
color = false
|
||||
close = func() {
|
||||
closeLog = func() {
|
||||
_ = bw.Flush()
|
||||
_ = logf.Close()
|
||||
}
|
||||
}
|
||||
|
||||
w = zerolog.SyncWriter(w)
|
||||
if json {
|
||||
log.Logger = log.Output(w)
|
||||
return close, nil
|
||||
return closeLog, nil
|
||||
}
|
||||
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{
|
||||
Out: w,
|
||||
NoColor: !color,
|
||||
})
|
||||
return close, nil
|
||||
|
||||
return closeLog, nil
|
||||
}
|
||||
|
||||
// removePIDFile removes the PID file if created.
|
||||
|
||||
26
go.mod
26
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
|
||||
@@ -10,16 +8,16 @@ require (
|
||||
github.com/google/subcommands v1.2.0
|
||||
github.com/gorilla/css v1.0.1
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/inbucket/gopher-json v0.2.0
|
||||
github.com/jhillyerd/enmime v1.1.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.26
|
||||
github.com/rs/zerolog v1.32.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
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.20.0
|
||||
golang.org/x/net v0.40.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -29,18 +27,18 @@ 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.15 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.6 // indirect
|
||||
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.16.0 // indirect
|
||||
golang.org/x/text v0.14.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
|
||||
)
|
||||
|
||||
49
go.sum
49
go.sum
@@ -10,8 +10,8 @@ github.com/cosmotek/loguago v1.0.0/go.mod h1:M/3wRiTLODLY6ufA9sVxOgSvnkYv53sYuDT
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
@@ -21,14 +21,14 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/inbucket/gopher-json v0.2.0 h1:v/luoFy5olitFhByVUGMZ3LmtcroRs9YHlyrBedz7EA=
|
||||
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 v1.1.0 h1:ubaIzg68VY7CMCe2YbHe6nkRvU9vujixTkNz3EBvZOw=
|
||||
github.com/jhillyerd/enmime v1.1.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
|
||||
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,17 +40,18 @@ 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=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
@@ -61,30 +62,30 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
|
||||
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
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.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
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=
|
||||
|
||||
@@ -51,7 +51,7 @@ func (n *mbNaming) Decode(v string) error {
|
||||
case "domain":
|
||||
*n = DomainNaming
|
||||
default:
|
||||
return fmt.Errorf("Unknown MailboxNaming strategy: %q", v)
|
||||
return fmt.Errorf("unknown MailboxNaming strategy: %q", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func (eb *AsyncEventBroker[E]) AsyncTestListener(name string, capacity int) func
|
||||
return &event, nil
|
||||
|
||||
case <-time.After(time.Second * 2):
|
||||
return nil, errors.New("Timeout waiting for event")
|
||||
return nil, errors.New("timeout waiting for event")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +47,13 @@ func TestAsyncBrokerEmitCallsMultipleListeners(t *testing.T) {
|
||||
want := "hi"
|
||||
broker.Emit(&want)
|
||||
|
||||
first_got, err := first()
|
||||
firstGot, err := first()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, *first_got)
|
||||
assert.Equal(t, want, *firstGot)
|
||||
|
||||
second_got, err := second()
|
||||
secondGot, err := second()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, *second_got)
|
||||
assert.Equal(t, want, *secondGot)
|
||||
}
|
||||
|
||||
func TestAsyncBrokerAddingDuplicateNameReplacesPrevious(t *testing.T) {
|
||||
@@ -66,13 +66,13 @@ func TestAsyncBrokerAddingDuplicateNameReplacesPrevious(t *testing.T) {
|
||||
want := "hi"
|
||||
broker.Emit(&want)
|
||||
|
||||
first_got, err := first()
|
||||
firstGot, err := first()
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, first_got)
|
||||
assert.Nil(t, firstGot)
|
||||
|
||||
second_got, err := second()
|
||||
secondGot, err := second()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, *second_got)
|
||||
assert.Equal(t, want, *secondGot)
|
||||
}
|
||||
|
||||
func TestAsyncBrokerRemovingListenerSuccessful(t *testing.T) {
|
||||
@@ -86,13 +86,13 @@ func TestAsyncBrokerRemovingListenerSuccessful(t *testing.T) {
|
||||
want := "hi"
|
||||
broker.Emit(&want)
|
||||
|
||||
first_got, err := first()
|
||||
firstGot, err := first()
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, first_got)
|
||||
assert.Nil(t, firstGot)
|
||||
|
||||
second_got, err := second()
|
||||
secondGot, err := second()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, *second_got)
|
||||
assert.Equal(t, want, *secondGot)
|
||||
}
|
||||
|
||||
func TestAsyncBrokerRemovingMissingListener(t *testing.T) {
|
||||
|
||||
@@ -28,13 +28,13 @@ func TestBrokerEmitCallsMultipleListeners(t *testing.T) {
|
||||
broker := &extension.EventBroker[string, bool]{}
|
||||
|
||||
// Setup listeners.
|
||||
var first_got, second_got string
|
||||
var firstGot, secondGot string
|
||||
first := func(s string) *bool {
|
||||
first_got = s
|
||||
firstGot = s
|
||||
return nil
|
||||
}
|
||||
second := func(s string) *bool {
|
||||
second_got = s
|
||||
secondGot = s
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -43,11 +43,11 @@ func TestBrokerEmitCallsMultipleListeners(t *testing.T) {
|
||||
|
||||
want := "hi"
|
||||
broker.Emit(&want)
|
||||
if first_got != want {
|
||||
t.Errorf("first got %q, want %q", first_got, want)
|
||||
if firstGot != want {
|
||||
t.Errorf("first got %q, want %q", firstGot, want)
|
||||
}
|
||||
if second_got != want {
|
||||
t.Errorf("second got %q, want %q", second_got, want)
|
||||
if secondGot != want {
|
||||
t.Errorf("second got %q, want %q", secondGot, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,13 +77,13 @@ func TestBrokerAddingDuplicateNameReplacesPrevious(t *testing.T) {
|
||||
broker := &extension.EventBroker[string, bool]{}
|
||||
|
||||
// Setup listeners.
|
||||
var first_got, second_got string
|
||||
var firstGot, secondGot string
|
||||
first := func(s string) *bool {
|
||||
first_got = s
|
||||
firstGot = s
|
||||
return nil
|
||||
}
|
||||
second := func(s string) *bool {
|
||||
second_got = s
|
||||
secondGot = s
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -92,11 +92,11 @@ func TestBrokerAddingDuplicateNameReplacesPrevious(t *testing.T) {
|
||||
|
||||
want := "hi"
|
||||
broker.Emit(&want)
|
||||
if first_got != "" {
|
||||
t.Errorf("first got %q, want empty string", first_got)
|
||||
if firstGot != "" {
|
||||
t.Errorf("first got %q, want empty string", firstGot)
|
||||
}
|
||||
if second_got != want {
|
||||
t.Errorf("second got %q, want %q", second_got, want)
|
||||
if secondGot != want {
|
||||
t.Errorf("second got %q, want %q", secondGot, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,13 +104,13 @@ func TestBrokerRemovingListenerSuccessful(t *testing.T) {
|
||||
broker := &extension.EventBroker[string, bool]{}
|
||||
|
||||
// Setup listeners.
|
||||
var first_got, second_got string
|
||||
var firstGot, secondGot string
|
||||
first := func(s string) *bool {
|
||||
first_got = s
|
||||
firstGot = s
|
||||
return nil
|
||||
}
|
||||
second := func(s string) *bool {
|
||||
second_got = s
|
||||
secondGot = s
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -120,11 +120,11 @@ func TestBrokerRemovingListenerSuccessful(t *testing.T) {
|
||||
|
||||
want := "hi"
|
||||
broker.Emit(&want)
|
||||
if first_got != "" {
|
||||
t.Errorf("first got %q, want empty string", first_got)
|
||||
if firstGot != "" {
|
||||
t.Errorf("first got %q, want empty string", firstGot)
|
||||
}
|
||||
if second_got != want {
|
||||
t.Errorf("second got %q, want %q", second_got, want)
|
||||
if secondGot != want {
|
||||
t.Errorf("second got %q, want %q", secondGot, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,15 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// ActionDefer defers decision to built-in Inbucket logic.
|
||||
ActionDefer = iota
|
||||
// ActionAllow explicitly allows this event.
|
||||
ActionAllow
|
||||
// ActionDeny explicitly deny this event, typically with specified SMTP error.
|
||||
ActionDeny
|
||||
)
|
||||
|
||||
// AddressParts contains the local and domain parts of an email address.
|
||||
type AddressParts struct {
|
||||
Local string
|
||||
@@ -31,3 +40,17 @@ type MessageMetadata struct {
|
||||
Size int64
|
||||
Seen bool
|
||||
}
|
||||
|
||||
// SMTPResponse describes the response to an SMTP policy check.
|
||||
type SMTPResponse struct {
|
||||
Action int // ActionDefer, ActionAllow, etc.
|
||||
ErrorCode int // SMTP error code to respond with on deny.
|
||||
ErrorMsg string // SMTP error message to respond with on deny.
|
||||
}
|
||||
|
||||
// SMTPSession captures SMTP `MAIL FROM` & `RCPT TO` values prior to mail DATA being received.
|
||||
type SMTPSession struct {
|
||||
From *mail.Address
|
||||
To []*mail.Address
|
||||
RemoteAddr string
|
||||
}
|
||||
|
||||
@@ -18,12 +18,13 @@ type Host struct {
|
||||
//
|
||||
// After-events allow extensions to take an action after an event has completed. These events are
|
||||
// processed asynchronously with respect to the rest of Inbuckets operation. However, an event
|
||||
// listener will not be called until the one before it complets.
|
||||
// listener will not be called until the one before it completes.
|
||||
type Events struct {
|
||||
AfterMessageDeleted AsyncEventBroker[event.MessageMetadata]
|
||||
AfterMessageStored AsyncEventBroker[event.MessageMetadata]
|
||||
BeforeMailAccepted EventBroker[event.AddressParts, bool]
|
||||
BeforeMessageStored EventBroker[event.InboundMessage, event.InboundMessage]
|
||||
AfterMessageDeleted AsyncEventBroker[event.MessageMetadata]
|
||||
AfterMessageStored AsyncEventBroker[event.MessageMetadata]
|
||||
BeforeMailFromAccepted EventBroker[event.SMTPSession, event.SMTPResponse]
|
||||
BeforeMessageStored EventBroker[event.InboundMessage, event.InboundMessage]
|
||||
BeforeRcptToAccepted EventBroker[event.SMTPSession, event.SMTPResponse]
|
||||
}
|
||||
|
||||
// Void indicates the event emitter will ignore any value returned by listeners.
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"net/mail"
|
||||
"testing"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func TestMailAddressGetters(t *testing.T) {
|
||||
@@ -26,8 +26,9 @@ func TestMailAddressGetters(t *testing.T) {
|
||||
assert(got == want, string.format("got address %q, want %q", got, want))
|
||||
`
|
||||
|
||||
ls := lua.NewState()
|
||||
ls, _ := test.NewLuaState()
|
||||
registerMailAddressType(ls)
|
||||
|
||||
ls.SetGlobal("addr", wrapMailAddress(ls, want))
|
||||
require.NoError(t, ls.DoString(script))
|
||||
}
|
||||
@@ -44,9 +45,10 @@ func TestMailAddressSetters(t *testing.T) {
|
||||
addr.address = "ri@example.com"
|
||||
`
|
||||
|
||||
got := &mail.Address{}
|
||||
ls := lua.NewState()
|
||||
ls, _ := test.NewLuaState()
|
||||
registerMailAddressType(ls)
|
||||
|
||||
got := &mail.Address{}
|
||||
ls.SetGlobal("addr", wrapMailAddress(ls, got))
|
||||
require.NoError(t, ls.DoString(script))
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ func unwrapInboundMessage(lv lua.LValue) (*event.InboundMessage, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Expected InboundMessage, got %q", lv.Type().String())
|
||||
return nil, fmt.Errorf("expected InboundMessage, got %q", lv.Type().String())
|
||||
}
|
||||
|
||||
// Gets a field value from InboundMessage user object. This emulates a Lua table,
|
||||
|
||||
@@ -5,29 +5,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// LuaInit holds useful test globals.
|
||||
const LuaInit = `
|
||||
function assert_eq(got, want)
|
||||
if type(got) == "table" and type(want) == "table" then
|
||||
assert(#got == #want, string.format("got %d element(s), wanted %d", #got, #want))
|
||||
|
||||
for i, gotv in ipairs(got) do
|
||||
local wantv = want[i]
|
||||
assert_eq(gotv, wantv, "got[%d] = %q, wanted %q", gotv, wantv)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
assert(got == want, string.format("got %q, wanted %q", got, want))
|
||||
end
|
||||
`
|
||||
|
||||
func TestInboundMessageGetters(t *testing.T) {
|
||||
want := &event.InboundMessage{
|
||||
Mailboxes: []string{"mb1", "mb2"},
|
||||
@@ -44,23 +26,23 @@ func TestInboundMessageGetters(t *testing.T) {
|
||||
|
||||
assert_eq(msg.mailboxes, {"mb1", "mb2"})
|
||||
assert_eq(msg.subject, "subj1")
|
||||
assert_eq(msg.size, 42)
|
||||
assert_eq(msg.size, 42, "msg.size")
|
||||
|
||||
assert_eq(msg.from.name, "name1")
|
||||
assert_eq(msg.from.address, "addr1")
|
||||
assert_eq(msg.from.name, "name1", "from.name")
|
||||
assert_eq(msg.from.address, "addr1", "from.address")
|
||||
|
||||
assert_eq(#msg.to, 2)
|
||||
assert_eq(msg.to[1].name, "name2")
|
||||
assert_eq(msg.to[1].address, "addr2")
|
||||
assert_eq(msg.to[2].name, "name3")
|
||||
assert_eq(msg.to[2].address, "addr3")
|
||||
assert_eq(#msg.to, 2, "#msg.to")
|
||||
assert_eq(msg.to[1].name, "name2", "to[1].name")
|
||||
assert_eq(msg.to[1].address, "addr2", "to[1].address")
|
||||
assert_eq(msg.to[2].name, "name3", "to[2].name")
|
||||
assert_eq(msg.to[2].address, "addr3", "to[2].address")
|
||||
`
|
||||
|
||||
ls := lua.NewState()
|
||||
ls, _ := test.NewLuaState()
|
||||
registerInboundMessageType(ls)
|
||||
registerMailAddressType(ls)
|
||||
ls.SetGlobal("msg", wrapInboundMessage(ls, want))
|
||||
require.NoError(t, ls.DoString(LuaInit+script))
|
||||
require.NoError(t, ls.DoString(script))
|
||||
}
|
||||
|
||||
func TestInboundMessageSetters(t *testing.T) {
|
||||
@@ -83,7 +65,7 @@ func TestInboundMessageSetters(t *testing.T) {
|
||||
`
|
||||
|
||||
got := &event.InboundMessage{}
|
||||
ls := lua.NewState()
|
||||
ls, _ := test.NewLuaState()
|
||||
registerInboundMessageType(ls)
|
||||
registerMailAddressType(ls)
|
||||
ls.SetGlobal("msg", wrapInboundMessage(ls, got))
|
||||
|
||||
@@ -29,8 +29,9 @@ type InbucketAfterFuncs struct {
|
||||
// InbucketBeforeFuncs holds references to Lua extension functions to be called
|
||||
// before Inbucket handles an event.
|
||||
type InbucketBeforeFuncs struct {
|
||||
MailAccepted *lua.LFunction
|
||||
MessageStored *lua.LFunction
|
||||
MailFromAccepted *lua.LFunction
|
||||
MessageStored *lua.LFunction
|
||||
RcptToAccepted *lua.LFunction
|
||||
}
|
||||
|
||||
func registerInbucketTypes(ls *lua.LState) {
|
||||
@@ -185,10 +186,12 @@ func inbucketBeforeIndex(ls *lua.LState) int {
|
||||
|
||||
// Push the requested field's value onto the stack.
|
||||
switch field {
|
||||
case "mail_accepted":
|
||||
ls.Push(funcOrNil(before.MailAccepted))
|
||||
case "mail_from_accepted":
|
||||
ls.Push(funcOrNil(before.MailFromAccepted))
|
||||
case "message_stored":
|
||||
ls.Push(funcOrNil(before.MessageStored))
|
||||
case "rcpt_to_accepted":
|
||||
ls.Push(funcOrNil(before.RcptToAccepted))
|
||||
default:
|
||||
// Unknown field.
|
||||
ls.Push(lua.LNil)
|
||||
@@ -203,10 +206,12 @@ func inbucketBeforeNewIndex(ls *lua.LState) int {
|
||||
index := ls.CheckString(2)
|
||||
|
||||
switch index {
|
||||
case "mail_accepted":
|
||||
m.MailAccepted = ls.CheckFunction(3)
|
||||
case "mail_from_accepted":
|
||||
m.MailFromAccepted = ls.CheckFunction(3)
|
||||
case "message_stored":
|
||||
m.MessageStored = ls.CheckFunction(3)
|
||||
case "rcpt_to_accepted":
|
||||
m.RcptToAccepted = ls.CheckFunction(3)
|
||||
default:
|
||||
ls.RaiseError("invalid inbucket.before index %q", index)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package luahost
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func TestInbucketAfterFuncs(t *testing.T) {
|
||||
@@ -49,7 +49,7 @@ func TestInbucketAfterFuncs(t *testing.T) {
|
||||
end
|
||||
`
|
||||
|
||||
ls := lua.NewState()
|
||||
ls, _ := test.NewLuaState()
|
||||
registerInbucketTypes(ls)
|
||||
require.NoError(t, ls.DoString(script))
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func TestInbucketBeforeFuncs(t *testing.T) {
|
||||
assert(inbucket, "inbucket should not be nil")
|
||||
assert(inbucket.before, "inbucket.before should not be nil")
|
||||
|
||||
local fns = { "mail_accepted", "message_stored" }
|
||||
local fns = { "mail_from_accepted", "message_stored", "rcpt_to_accepted" }
|
||||
|
||||
-- Verify functions start off nil.
|
||||
for i, name in ipairs(fns) do
|
||||
@@ -96,7 +96,7 @@ func TestInbucketBeforeFuncs(t *testing.T) {
|
||||
end
|
||||
`
|
||||
|
||||
ls := lua.NewState()
|
||||
ls, _ := test.NewLuaState()
|
||||
registerInbucketTypes(ls)
|
||||
require.NoError(t, ls.DoString(script))
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func TestMessageMetadataGetters(t *testing.T) {
|
||||
@@ -24,26 +24,22 @@ func TestMessageMetadataGetters(t *testing.T) {
|
||||
script := `
|
||||
assert(msg, "msg should not be nil")
|
||||
|
||||
function assert_eq(got, want)
|
||||
assert(got == want, string.format("got name %q, wanted %q", got, want))
|
||||
end
|
||||
|
||||
assert_eq(msg.mailbox, "mb1")
|
||||
assert_eq(msg.id, "id1")
|
||||
assert_eq(msg.subject, "subj1")
|
||||
assert_eq(msg.size, 42)
|
||||
assert_eq(msg.size, 42, "msg.size")
|
||||
|
||||
assert_eq(msg.from.name, "name1")
|
||||
assert_eq(msg.from.address, "addr1")
|
||||
assert_eq(msg.from.name, "name1", "from.name")
|
||||
assert_eq(msg.from.address, "addr1", "from.address")
|
||||
|
||||
assert_eq(table.getn(msg.to), 1)
|
||||
assert_eq(msg.to[1].name, "name2")
|
||||
assert_eq(msg.to[1].address, "addr2")
|
||||
assert_eq(msg.to[1].name, "name2", "to.name")
|
||||
assert_eq(msg.to[1].address, "addr2", "to.address")
|
||||
|
||||
assert_eq(msg.date, 981173106)
|
||||
assert_eq(msg.date, 981173106, "msg.date")
|
||||
`
|
||||
|
||||
ls := lua.NewState()
|
||||
ls, _ := test.NewLuaState()
|
||||
registerMessageMetadataType(ls)
|
||||
registerMailAddressType(ls)
|
||||
ls.SetGlobal("msg", wrapMessageMetadata(ls, want))
|
||||
@@ -75,7 +71,7 @@ func TestMessageMetadataSetters(t *testing.T) {
|
||||
`
|
||||
|
||||
got := &event.MessageMetadata{}
|
||||
ls := lua.NewState()
|
||||
ls, _ := test.NewLuaState()
|
||||
registerMessageMetadataType(ls)
|
||||
registerMailAddressType(ls)
|
||||
ls.SetGlobal("msg", wrapMessageMetadata(ls, got))
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package luahost
|
||||
|
||||
import (
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const policyName = "policy"
|
||||
|
||||
func registerPolicyType(ls *lua.LState) {
|
||||
mt := ls.NewTypeMetatable(policyName)
|
||||
ls.SetGlobal(policyName, mt)
|
||||
|
||||
// Static attributes.
|
||||
ls.SetField(mt, "allow", lua.LTrue)
|
||||
ls.SetField(mt, "deny", lua.LFalse)
|
||||
ls.SetField(mt, "defer", lua.LNil)
|
||||
}
|
||||
54
pkg/extension/luahost/bind_smtpresponse.go
Normal file
54
pkg/extension/luahost/bind_smtpresponse.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package luahost
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const smtpResponseName = "smtp"
|
||||
|
||||
func registerSMTPResponseType(ls *lua.LState) {
|
||||
mt := ls.NewTypeMetatable(smtpResponseName)
|
||||
ls.SetGlobal(smtpResponseName, mt)
|
||||
|
||||
// Static attributes.
|
||||
ls.SetField(mt, "allow", ls.NewFunction(newSMTPResponse(event.ActionAllow)))
|
||||
ls.SetField(mt, "defer", ls.NewFunction(newSMTPResponse(event.ActionDefer)))
|
||||
ls.SetField(mt, "deny", ls.NewFunction(newSMTPResponse(event.ActionDeny)))
|
||||
}
|
||||
|
||||
func newSMTPResponse(action int) func(*lua.LState) int {
|
||||
return func(ls *lua.LState) int {
|
||||
val := &event.SMTPResponse{Action: action}
|
||||
|
||||
if action == event.ActionDeny {
|
||||
// Optionally accept error code and message.
|
||||
val.ErrorCode = ls.OptInt(1, 550)
|
||||
val.ErrorMsg = ls.OptString(2, "Mail denied by policy")
|
||||
}
|
||||
|
||||
ud := wrapSMTPResponse(ls, val)
|
||||
ls.Push(ud)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func wrapSMTPResponse(ls *lua.LState, val *event.SMTPResponse) *lua.LUserData {
|
||||
ud := ls.NewUserData()
|
||||
ud.Value = val
|
||||
ls.SetMetatable(ud, ls.GetTypeMetatable(smtpResponseName))
|
||||
|
||||
return ud
|
||||
}
|
||||
|
||||
func unwrapSMTPResponse(lv lua.LValue) (*event.SMTPResponse, error) {
|
||||
if ud, ok := lv.(*lua.LUserData); ok {
|
||||
if v, ok := ud.Value.(*event.SMTPResponse); ok {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("expected SMTPResponse, got %q", lv.Type().String())
|
||||
}
|
||||
40
pkg/extension/luahost/bind_smtpresponse_test.go
Normal file
40
pkg/extension/luahost/bind_smtpresponse_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package luahost
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSMTPResponseConstructors(t *testing.T) {
|
||||
check := func(script string, want event.SMTPResponse) {
|
||||
t.Helper()
|
||||
ls, _ := test.NewLuaState()
|
||||
registerSMTPResponseType(ls)
|
||||
require.NoError(t, ls.DoString(script))
|
||||
|
||||
got, err := unwrapSMTPResponse(ls.Get(-1))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &want, got)
|
||||
}
|
||||
|
||||
check("return smtp.defer()", event.SMTPResponse{Action: event.ActionDefer})
|
||||
check("return smtp.allow()", event.SMTPResponse{Action: event.ActionAllow})
|
||||
|
||||
// Verify deny() has default code & msg.
|
||||
check("return smtp.deny()", event.SMTPResponse{
|
||||
Action: event.ActionDeny,
|
||||
ErrorCode: 550,
|
||||
ErrorMsg: "Mail denied by policy",
|
||||
})
|
||||
|
||||
// Verify defaults can be overridden.
|
||||
check("return smtp.deny(123, 'bacon')", event.SMTPResponse{
|
||||
Action: event.ActionDeny,
|
||||
ErrorCode: 123,
|
||||
ErrorMsg: "bacon",
|
||||
})
|
||||
}
|
||||
72
pkg/extension/luahost/bind_smtpsession.go
Normal file
72
pkg/extension/luahost/bind_smtpsession.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package luahost
|
||||
|
||||
import (
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const smtpSessionName = "smtp_session"
|
||||
|
||||
func registerSMTPSessionType(ls *lua.LState) {
|
||||
mt := ls.NewTypeMetatable(smtpSessionName)
|
||||
ls.SetGlobal(smtpSessionName, mt)
|
||||
|
||||
// Static attributes.
|
||||
ls.SetField(mt, "new", ls.NewFunction(newSMTPSession))
|
||||
|
||||
// Methods.
|
||||
ls.SetField(mt, "__index", ls.NewFunction(smtpSessionIndex))
|
||||
}
|
||||
|
||||
func newSMTPSession(ls *lua.LState) int {
|
||||
val := &event.SMTPSession{}
|
||||
ud := wrapSMTPSession(ls, val)
|
||||
ls.Push(ud)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func wrapSMTPSession(ls *lua.LState, val *event.SMTPSession) *lua.LUserData {
|
||||
ud := ls.NewUserData()
|
||||
ud.Value = val
|
||||
ls.SetMetatable(ud, ls.GetTypeMetatable(smtpSessionName))
|
||||
|
||||
return ud
|
||||
}
|
||||
|
||||
// Checks there is an SMTPSession at stack position `pos`, else throws Lua error.
|
||||
func checkSMTPSession(ls *lua.LState, pos int) *event.SMTPSession {
|
||||
ud := ls.CheckUserData(pos)
|
||||
if v, ok := ud.Value.(*event.SMTPSession); ok {
|
||||
return v
|
||||
}
|
||||
ls.ArgError(pos, smtpSessionName+" expected")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Gets a field value from SMTPSession user object. This emulates a Lua table,
|
||||
// allowing `msg.subject` instead of a Lua object syntax of `msg:subject()`.
|
||||
func smtpSessionIndex(ls *lua.LState) int {
|
||||
session := checkSMTPSession(ls, 1)
|
||||
field := ls.CheckString(2)
|
||||
|
||||
// Push the requested field's value onto the stack.
|
||||
switch field {
|
||||
case "from":
|
||||
ls.Push(wrapMailAddress(ls, session.From))
|
||||
case "to":
|
||||
lt := &lua.LTable{}
|
||||
for _, v := range session.To {
|
||||
addr := v
|
||||
lt.Append(wrapMailAddress(ls, addr))
|
||||
}
|
||||
ls.Push(lt)
|
||||
case "remote_addr":
|
||||
ls.Push(lua.LString(session.RemoteAddr))
|
||||
default:
|
||||
// Unknown field.
|
||||
ls.Push(lua.LNil)
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
41
pkg/extension/luahost/bind_smtpsession_test.go
Normal file
41
pkg/extension/luahost/bind_smtpsession_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package luahost
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
"testing"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSMTPSessionGetters(t *testing.T) {
|
||||
want := &event.SMTPSession{
|
||||
From: &mail.Address{Name: "name1", Address: "addr1"},
|
||||
To: []*mail.Address{
|
||||
{Name: "name2", Address: "addr2"},
|
||||
{Name: "name3", Address: "addr3"},
|
||||
},
|
||||
RemoteAddr: "1.2.3.4",
|
||||
}
|
||||
script := `
|
||||
assert(session, "session should not be nil")
|
||||
|
||||
assert_eq(session.from.name, "name1", "from.name")
|
||||
assert_eq(session.from.address, "addr1", "from.address")
|
||||
|
||||
assert_eq(#session.to, 2, "#session.to")
|
||||
assert_eq(session.to[1].name, "name2", "to[1].name")
|
||||
assert_eq(session.to[1].address, "addr2", "to[1].address")
|
||||
assert_eq(session.to[2].name, "name3", "to[2].name")
|
||||
assert_eq(session.to[2].address, "addr3", "to[2].address")
|
||||
|
||||
assert_eq(session.remote_addr, "1.2.3.4")
|
||||
`
|
||||
|
||||
ls, _ := test.NewLuaState()
|
||||
registerSMTPSessionType(ls)
|
||||
registerMailAddressType(ls)
|
||||
ls.SetGlobal("session", wrapSMTPSession(ls, want))
|
||||
require.NoError(t, ls.DoString(script))
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package luahost
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -15,6 +16,9 @@ import (
|
||||
"github.com/yuin/gopher-lua/parse"
|
||||
)
|
||||
|
||||
// ErrNoScript signals that the Lua script file was not present.
|
||||
var ErrNoScript error = errors.New("no script file present")
|
||||
|
||||
// Host of Lua extensions.
|
||||
type Host struct {
|
||||
extHost *extension.Host
|
||||
@@ -34,10 +38,10 @@ 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")
|
||||
return nil, nil
|
||||
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)
|
||||
return nil, fmt.Errorf("lua script %v is a directory", scriptPath)
|
||||
}
|
||||
|
||||
logger.Info().Msg("Loading script")
|
||||
@@ -102,12 +106,15 @@ func (h *Host) wireFunctions(logger zerolog.Logger, ls *lua.LState) {
|
||||
if ib.After.MessageStored != nil {
|
||||
events.AfterMessageStored.AddListener(listenerName, h.handleAfterMessageStored)
|
||||
}
|
||||
if ib.Before.MailAccepted != nil {
|
||||
events.BeforeMailAccepted.AddListener(listenerName, h.handleBeforeMailAccepted)
|
||||
if ib.Before.MailFromAccepted != nil {
|
||||
events.BeforeMailFromAccepted.AddListener(listenerName, h.handleBeforeMailFromAccepted)
|
||||
}
|
||||
if ib.Before.MessageStored != nil {
|
||||
events.BeforeMessageStored.AddListener(listenerName, h.handleBeforeMessageStored)
|
||||
}
|
||||
if ib.Before.RcptToAccepted != nil {
|
||||
events.BeforeRcptToAccepted.AddListener(listenerName, h.handleBeforeRcptToAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Host) handleAfterMessageDeleted(msg event.MessageMetadata) {
|
||||
@@ -144,37 +151,60 @@ func (h *Host) handleAfterMessageStored(msg event.MessageMetadata) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Host) handleBeforeMailAccepted(addr event.AddressParts) *bool {
|
||||
logger, ls, ib, ok := h.prepareInbucketFuncCall("before.mail_accepted")
|
||||
func (h *Host) handleBeforeMailFromAccepted(session event.SMTPSession) *event.SMTPResponse {
|
||||
logger, ls, ib, ok := h.prepareInbucketFuncCall("before.mail_from_accepted")
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
defer h.pool.putState(ls)
|
||||
|
||||
logger.Debug().Msgf("Calling Lua function with %+v", addr)
|
||||
logger.Debug().Msgf("Calling Lua function with %+v", session)
|
||||
if err := ls.CallByParam(
|
||||
lua.P{Fn: ib.Before.MailAccepted, NRet: 1, Protect: true},
|
||||
lua.LString(addr.Local),
|
||||
lua.LString(addr.Domain),
|
||||
lua.P{Fn: ib.Before.MailFromAccepted, NRet: 1, Protect: true},
|
||||
wrapSMTPSession(ls, &session),
|
||||
); err != nil {
|
||||
logger.Error().Err(err).Msg("Failed to call Lua function")
|
||||
return nil
|
||||
}
|
||||
|
||||
lval := ls.Get(1)
|
||||
lval := ls.Get(-1)
|
||||
ls.Pop(1)
|
||||
logger.Debug().Msgf("Lua function returned %q (%v)", lval, lval.Type().String())
|
||||
|
||||
if lval.Type() == lua.LTNil {
|
||||
result, err := unwrapSMTPResponse(lval)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Bad response from Lua Function")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *Host) handleBeforeRcptToAccepted(session event.SMTPSession) *event.SMTPResponse {
|
||||
logger, ls, ib, ok := h.prepareInbucketFuncCall("before.rcpt_to_accepted")
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
defer h.pool.putState(ls)
|
||||
|
||||
logger.Debug().Msgf("Calling Lua function with %+v", session)
|
||||
if err := ls.CallByParam(
|
||||
lua.P{Fn: ib.Before.RcptToAccepted, NRet: 1, Protect: true},
|
||||
wrapSMTPSession(ls, &session),
|
||||
); err != nil {
|
||||
logger.Error().Err(err).Msg("Failed to call Lua function")
|
||||
return nil
|
||||
}
|
||||
|
||||
result := true
|
||||
if lua.LVIsFalse(lval) {
|
||||
result = false
|
||||
lval := ls.Get(-1)
|
||||
ls.Pop(1)
|
||||
logger.Debug().Msgf("Lua function returned %q (%v)", lval, lval.Type().String())
|
||||
|
||||
result, err := unwrapSMTPResponse(lval)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Bad response from Lua Function")
|
||||
}
|
||||
|
||||
return &result
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *Host) handleBeforeMessageStored(msg event.InboundMessage) *event.InboundMessage {
|
||||
@@ -193,10 +223,11 @@ func (h *Host) handleBeforeMessageStored(msg event.InboundMessage) *event.Inboun
|
||||
return nil
|
||||
}
|
||||
|
||||
lval := ls.Get(1)
|
||||
lval := ls.Get(-1)
|
||||
ls.Pop(1)
|
||||
logger.Debug().Msgf("Lua function returned %q (%v)", lval, lval.Type().String())
|
||||
|
||||
if lval.Type() == lua.LTNil || lua.LVIsFalse(lval) {
|
||||
if lua.LVIsFalse(lval) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -204,7 +235,6 @@ func (h *Host) handleBeforeMessageStored(msg event.InboundMessage) *event.Inboun
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Bad response from Lua Function")
|
||||
}
|
||||
ls.Pop(1)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -9,54 +9,12 @@ import (
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension"
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/luahost"
|
||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// LuaInit holds useful test globals.
|
||||
const LuaInit = `
|
||||
local logger = require("logger")
|
||||
|
||||
async = false
|
||||
test_ok = true
|
||||
|
||||
-- Sends marks tests as failed instead of erroring when enabled.
|
||||
function assert_async(value, message)
|
||||
if not value then
|
||||
if async then
|
||||
logger.error(message, {from = "assert_async"})
|
||||
test_ok = false
|
||||
else
|
||||
error(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Verifies plain values and list-style tables.
|
||||
function assert_eq(got, want)
|
||||
if type(got) == "table" and type(want) == "table" then
|
||||
assert_async(#got == #want, string.format("got %d elements, wanted %d", #got, #want))
|
||||
|
||||
for i, gotv in ipairs(got) do
|
||||
local wantv = want[i]
|
||||
assert_eq(gotv, wantv, "got[%d] = %q, wanted %q", gotv, wantv)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
assert_async(got == want, string.format("got %q, wanted %q", got, want))
|
||||
end
|
||||
|
||||
-- Verifies string want contains string got.
|
||||
function assert_contains(got, want)
|
||||
assert_async(string.find(got, want),
|
||||
string.format("got %q, wanted it to contain %q", got, want))
|
||||
end
|
||||
`
|
||||
|
||||
var consoleLogger = zerolog.New(zerolog.NewConsoleWriter())
|
||||
|
||||
func TestEmptyScript(t *testing.T) {
|
||||
@@ -92,11 +50,12 @@ func TestAfterMessageDeleted(t *testing.T) {
|
||||
-- Full message bindings tested elsewhere.
|
||||
assert_eq(msg.mailbox, "mb1")
|
||||
assert_eq(msg.id, "id1")
|
||||
notify:send(test_ok)
|
||||
notify:send(asserts_ok)
|
||||
end
|
||||
`
|
||||
extHost := extension.NewHost()
|
||||
luaHost, err := luahost.NewFromReader(consoleLogger, extHost, strings.NewReader(LuaInit+script), "test.lua")
|
||||
luaHost, err := luahost.NewFromReader(consoleLogger, extHost,
|
||||
strings.NewReader(test.LuaInit+script), "test.lua")
|
||||
require.NoError(t, err)
|
||||
notify := luaHost.CreateChannel("notify")
|
||||
|
||||
@@ -111,7 +70,7 @@ func TestAfterMessageDeleted(t *testing.T) {
|
||||
Size: 42,
|
||||
}
|
||||
extHost.Events.AfterMessageDeleted.Emit(msg)
|
||||
assertNotified(t, notify)
|
||||
test.AssertNotified(t, notify)
|
||||
}
|
||||
|
||||
func TestAfterMessageStored(t *testing.T) {
|
||||
@@ -123,11 +82,12 @@ func TestAfterMessageStored(t *testing.T) {
|
||||
-- Full message bindings tested elsewhere.
|
||||
assert_eq(msg.mailbox, "mb1")
|
||||
assert_eq(msg.id, "id1")
|
||||
notify:send(test_ok)
|
||||
notify:send(asserts_ok)
|
||||
end
|
||||
`
|
||||
extHost := extension.NewHost()
|
||||
luaHost, err := luahost.NewFromReader(consoleLogger, extHost, strings.NewReader(LuaInit+script), "test.lua")
|
||||
luaHost, err := luahost.NewFromReader(consoleLogger, extHost,
|
||||
strings.NewReader(test.LuaInit+script), "test.lua")
|
||||
require.NoError(t, err)
|
||||
notify := luaHost.CreateChannel("notify")
|
||||
|
||||
@@ -142,36 +102,51 @@ func TestAfterMessageStored(t *testing.T) {
|
||||
Size: 42,
|
||||
}
|
||||
extHost.Events.AfterMessageStored.Emit(msg)
|
||||
assertNotified(t, notify)
|
||||
test.AssertNotified(t, notify)
|
||||
}
|
||||
|
||||
func TestBeforeMailAccepted(t *testing.T) {
|
||||
func TestBeforeMailFromAccepted(t *testing.T) {
|
||||
// Register lua event listener.
|
||||
script := `
|
||||
function inbucket.before.mail_accepted(localpart, domain)
|
||||
return localpart == "from" and domain == "test"
|
||||
function inbucket.before.mail_from_accepted(session)
|
||||
if session.from.address == "from@example.com" then
|
||||
logger.info("allowing message", {})
|
||||
return smtp.allow()
|
||||
else
|
||||
logger.info("denying message", {})
|
||||
return smtp.deny()
|
||||
end
|
||||
end
|
||||
`
|
||||
extHost := extension.NewHost()
|
||||
_, err := luahost.NewFromReader(consoleLogger, extHost, strings.NewReader(script), "test.lua")
|
||||
_, err := luahost.NewFromReader(
|
||||
consoleLogger, extHost, strings.NewReader(test.LuaInit+script), "test.lua")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Send event to be accepted.
|
||||
addr := &event.AddressParts{Local: "from", Domain: "test"}
|
||||
got := extHost.Events.BeforeMailAccepted.Emit(addr)
|
||||
want := true
|
||||
require.NotNil(t, got, "Expected result from Emit()")
|
||||
if *got != want {
|
||||
t.Errorf("Got %v, wanted %v for addr %v", *got, want, addr)
|
||||
{
|
||||
// Send event to be accepted.
|
||||
session := event.SMTPSession{
|
||||
From: &mail.Address{Name: "", Address: "from@example.com"},
|
||||
}
|
||||
got := extHost.Events.BeforeMailFromAccepted.Emit(&session)
|
||||
want := event.ActionAllow
|
||||
require.NotNil(t, got, "Expected result from Emit()")
|
||||
if got.Action != want {
|
||||
t.Errorf("Got %v, wanted %v for addr %v", got.Action, want, session.From)
|
||||
}
|
||||
}
|
||||
|
||||
// Send event to be denied.
|
||||
addr = &event.AddressParts{Local: "reject", Domain: "me"}
|
||||
got = extHost.Events.BeforeMailAccepted.Emit(addr)
|
||||
want = false
|
||||
require.NotNil(t, got, "Expected result from Emit()")
|
||||
if *got != want {
|
||||
t.Errorf("Got %v, wanted %v for addr %v", *got, want, addr)
|
||||
{
|
||||
// Send event to be denied.
|
||||
session := event.SMTPSession{
|
||||
From: &mail.Address{Name: "", Address: "from@reject.com"},
|
||||
}
|
||||
got := extHost.Events.BeforeMailFromAccepted.Emit(&session)
|
||||
want := event.ActionDeny
|
||||
require.NotNil(t, got, "Expected result from Emit()")
|
||||
if got.Action != want {
|
||||
t.Errorf("Got %v, wanted %v for addr %v", got.Action, want, session.From)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,14 +172,14 @@ func TestBeforeMessageStored(t *testing.T) {
|
||||
assert_eq(msg.mailboxes, {"one", "two"})
|
||||
assert_eq(msg.from.name, "From Name")
|
||||
assert_eq(msg.from.address, "from@example.com")
|
||||
assert_eq(2, #msg.to)
|
||||
assert_eq(2, #msg.to, "#msg.to")
|
||||
assert_eq(msg.to[1].name, "To1 Name")
|
||||
assert_eq(msg.to[1].address, "to1@example.com")
|
||||
assert_eq(msg.to[2].name, "To2 Name")
|
||||
assert_eq(msg.to[2].address, "to2@example.com")
|
||||
assert_eq(msg.subject, "inbound subj")
|
||||
assert_eq(msg.size, 42)
|
||||
notify:send(test_ok)
|
||||
assert_eq(msg.size, 42, "msg.size")
|
||||
notify:send(asserts_ok)
|
||||
|
||||
-- Generate response.
|
||||
res = inbound_message.new()
|
||||
@@ -219,7 +194,8 @@ func TestBeforeMessageStored(t *testing.T) {
|
||||
end
|
||||
`
|
||||
extHost := extension.NewHost()
|
||||
luaHost, err := luahost.NewFromReader(consoleLogger, extHost, strings.NewReader(LuaInit+script), "test.lua")
|
||||
luaHost, err := luahost.NewFromReader(consoleLogger, extHost,
|
||||
strings.NewReader(test.LuaInit+script), "test.lua")
|
||||
require.NoError(t, err)
|
||||
notify := luaHost.CreateChannel("notify")
|
||||
|
||||
@@ -228,7 +204,7 @@ func TestBeforeMessageStored(t *testing.T) {
|
||||
require.NotNil(t, got, "Expected result from Emit()")
|
||||
|
||||
// Verify Lua assertions passed.
|
||||
assertNotified(t, notify)
|
||||
test.AssertNotified(t, notify)
|
||||
|
||||
// Verify response values.
|
||||
want := &event.InboundMessage{
|
||||
@@ -244,15 +220,84 @@ func TestBeforeMessageStored(t *testing.T) {
|
||||
assert.Equal(t, want, got, "Response InboundMessage did not match")
|
||||
}
|
||||
|
||||
func assertNotified(t *testing.T, notify chan lua.LValue) {
|
||||
t.Helper()
|
||||
select {
|
||||
case reslv := <-notify:
|
||||
// Lua function received event.
|
||||
if lua.LVIsFalse(reslv) {
|
||||
t.Error("Lua responsed with false, wanted true")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("Lua did not respond to event within timeout")
|
||||
func TestBeforeMessageStoredNilReturn(t *testing.T) {
|
||||
// Event to send.
|
||||
msg := event.InboundMessage{
|
||||
Mailboxes: []string{"one", "two"},
|
||||
From: &mail.Address{Name: "From Name", Address: "from@example.com"},
|
||||
To: []*mail.Address{
|
||||
{Name: "To1 Name", Address: "to1@example.com"},
|
||||
{Name: "To2 Name", Address: "to2@example.com"},
|
||||
},
|
||||
Subject: "inbound subj",
|
||||
Size: 42,
|
||||
}
|
||||
|
||||
// Register lua event listener.
|
||||
script := `
|
||||
async = true
|
||||
|
||||
function inbucket.before.message_stored(msg)
|
||||
assert(msg)
|
||||
notify:send(asserts_ok)
|
||||
|
||||
-- Generate response.
|
||||
return nil
|
||||
end
|
||||
`
|
||||
extHost := extension.NewHost()
|
||||
luaHost, err := luahost.NewFromReader(consoleLogger, extHost,
|
||||
strings.NewReader(test.LuaInit+script), "test.lua")
|
||||
require.NoError(t, err)
|
||||
notify := luaHost.CreateChannel("notify")
|
||||
|
||||
// Send event to be accepted.
|
||||
got := extHost.Events.BeforeMessageStored.Emit(&msg)
|
||||
require.Nil(t, got, "Expected nil result from Emit()")
|
||||
|
||||
// Verify Lua assertions passed.
|
||||
test.AssertNotified(t, notify)
|
||||
}
|
||||
|
||||
func TestBeforeRcptToAccepted(t *testing.T) {
|
||||
// Event to send.
|
||||
session := event.SMTPSession{
|
||||
From: &mail.Address{Name: "", Address: "from@example.com"},
|
||||
To: []*mail.Address{
|
||||
{Name: "", Address: "to1@example.com"},
|
||||
{Name: "", Address: "to2@example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
// Register lua event listener.
|
||||
script := `
|
||||
async = true
|
||||
|
||||
function inbucket.before.rcpt_to_accepted(msg)
|
||||
-- Verify incoming values.
|
||||
assert_eq(msg.from.address, "from@example.com")
|
||||
assert_eq(2, #msg.to, "#msg.to")
|
||||
assert_eq(msg.to[1].address, "to1@example.com")
|
||||
assert_eq(msg.to[2].address, "to2@example.com")
|
||||
notify:send(asserts_ok)
|
||||
|
||||
return smtp.allow()
|
||||
end
|
||||
`
|
||||
extHost := extension.NewHost()
|
||||
luaHost, err := luahost.NewFromReader(consoleLogger, extHost,
|
||||
strings.NewReader(test.LuaInit+script), "test.lua")
|
||||
require.NoError(t, err)
|
||||
notify := luaHost.CreateChannel("notify")
|
||||
|
||||
// Send event to be accepted.
|
||||
got := extHost.Events.BeforeRcptToAccepted.Emit(&session)
|
||||
require.NotNil(t, got, "Expected result from Emit()")
|
||||
|
||||
// Verify Lua assertions passed.
|
||||
test.AssertNotified(t, notify)
|
||||
|
||||
// Verify response values.
|
||||
want := event.SMTPResponse{Action: event.ActionAllow}
|
||||
assert.Equal(t, want, *got)
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ func (lp *statePool) newState() (*lua.LState, error) {
|
||||
registerInbucketTypes(ls)
|
||||
registerMailAddressType(ls)
|
||||
registerMessageMetadataType(ls)
|
||||
registerPolicyType(ls)
|
||||
registerSMTPResponseType(ls)
|
||||
registerSMTPSessionType(ls)
|
||||
|
||||
// Run compiled script.
|
||||
ls.Push(ls.NewFunctionFromProto(lp.funcProto))
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestPoolGrowsWithPuts(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
b, err := pool.getState()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(pool.states), "Wanted pool to be empty")
|
||||
assert.Empty(t, pool.states, "Wanted pool to be empty")
|
||||
|
||||
pool.putState(a)
|
||||
pool.putState(b)
|
||||
@@ -64,11 +64,11 @@ func TestPoolPutDiscardsClosed(t *testing.T) {
|
||||
|
||||
a, err := pool.getState()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(pool.states), "Wanted pool to be empty")
|
||||
assert.Empty(t, pool.states, "Wanted pool to be empty")
|
||||
|
||||
a.Close()
|
||||
pool.putState(a)
|
||||
assert.Equal(t, 0, len(pool.states), "Wanted pool to remain empty")
|
||||
assert.Empty(t, pool.states, "Wanted pool to remain empty")
|
||||
}
|
||||
|
||||
func TestPoolPutClearsStack(t *testing.T) {
|
||||
@@ -76,7 +76,7 @@ func TestPoolPutClearsStack(t *testing.T) {
|
||||
|
||||
ls, err := pool.getState()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(pool.states), "Wanted pool to be empty")
|
||||
assert.Empty(t, pool.states, "Wanted pool to be empty")
|
||||
|
||||
// Setup stack.
|
||||
ls.Push(lua.LNumber(4))
|
||||
@@ -85,7 +85,7 @@ func TestPoolPutClearsStack(t *testing.T) {
|
||||
|
||||
// Return and verify stack cleared.
|
||||
pool.putState(ls)
|
||||
assert.Equal(t, 1, len(pool.states), "Wanted pool to have one item")
|
||||
assert.Len(t, pool.states, 1, "Wanted pool to have one item")
|
||||
require.Equal(t, 0, ls.GetTop(), "Want stack to be empty")
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
"github.com/inbucket/inbucket/v3/pkg/policy"
|
||||
"github.com/inbucket/inbucket/v3/pkg/storage"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"github.com/jhillyerd/enmime/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -79,9 +79,9 @@ func (s *StoreManager) Deliver(
|
||||
tstamp := now.UTC().Format(recvdTimeFmt)
|
||||
|
||||
// Process inbound message through extensions.
|
||||
mailboxes := make([]string, len(recipients))
|
||||
for i, recip := range recipients {
|
||||
mailboxes[i] = recip.Mailbox
|
||||
mailboxes := make([]string, 0, len(recipients))
|
||||
for _, recip := range recipients {
|
||||
mailboxes = append(mailboxes, recip.Mailbox)
|
||||
}
|
||||
|
||||
// Construct InboundMessage event and process through extensions.
|
||||
@@ -110,9 +110,9 @@ func (s *StoreManager) Deliver(
|
||||
|
||||
// Deliver to each mailbox.
|
||||
for _, mb := range inbound.Mailboxes {
|
||||
// Append recipient and timestamp to generated Recieved header.
|
||||
// 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{
|
||||
@@ -122,8 +122,9 @@ func (s *StoreManager) Deliver(
|
||||
To: inbound.To,
|
||||
Date: now,
|
||||
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 {
|
||||
|
||||
@@ -312,7 +312,7 @@ func TestDeliverEmitsAfterMessageStoredEvent(t *testing.T) {
|
||||
origin,
|
||||
[]*policy.Recipient{recip},
|
||||
"Received: xyz\n",
|
||||
[]byte("From: from@example.com\n\ntest email"),
|
||||
[]byte("From: from@example.com\nSubject: events\n\ntest email."),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -321,6 +321,17 @@ func TestDeliverEmitsAfterMessageStoredEvent(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, got, "No event received, or it was nil")
|
||||
assertMessageCount(t, sm, "to@example.com", 1)
|
||||
|
||||
// Verify event content.
|
||||
assert.Equal(t, "to@example.com", got.Mailbox)
|
||||
assert.Equal(t, "from@example.com", got.From.Address)
|
||||
|
||||
assert.WithinDuration(t, time.Now(), got.Date, 5*time.Second)
|
||||
assert.Equal(t, "events", got.Subject, nil)
|
||||
assert.Equal(t, int64(51), got.Size)
|
||||
|
||||
require.Len(t, got.To, 1)
|
||||
assert.Equal(t, "to@example.com", got.To[0].Address)
|
||||
}
|
||||
|
||||
func TestDeliverBeforeAndAfterMessageStoredEvents(t *testing.T) {
|
||||
@@ -352,7 +363,7 @@ func TestDeliverBeforeAndAfterMessageStoredEvents(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Confirm mailbox names overriden by Before were sent to After event. Order is
|
||||
// Confirm mailbox names overridden by `Before` were sent to `After` event. Order is
|
||||
// not guaranteed.
|
||||
got1, err := listener()
|
||||
require.NoError(t, err)
|
||||
@@ -368,10 +379,10 @@ func TestGetMessage(t *testing.T) {
|
||||
|
||||
// Add a test message.
|
||||
subject := "getMessage1"
|
||||
id := addTestMessage(sm, "box1", subject)
|
||||
id := addTestMessage(sm, "get-box", subject)
|
||||
|
||||
// Verify retrieval of the test message.
|
||||
msg, err := sm.GetMessage("box1", id)
|
||||
msg, err := sm.GetMessage("get-box", id)
|
||||
require.NoError(t, err, "GetMessage must succeed")
|
||||
require.NotNil(t, msg, "GetMessage must return a result")
|
||||
assert.Equal(t, subject, msg.Subject)
|
||||
@@ -383,19 +394,19 @@ func TestMarkSeen(t *testing.T) {
|
||||
|
||||
// Add a test message.
|
||||
subject := "getMessage1"
|
||||
id := addTestMessage(sm, "box1", subject)
|
||||
id := addTestMessage(sm, "seen-box", subject)
|
||||
|
||||
// Verify test message unseen.
|
||||
msg, err := sm.GetMessage("box1", id)
|
||||
msg, err := sm.GetMessage("seen-box", id)
|
||||
require.NoError(t, err, "GetMessage must succeed")
|
||||
require.NotNil(t, msg, "GetMessage must return a result")
|
||||
assert.False(t, msg.Seen, "msg should be unseen")
|
||||
|
||||
err = sm.MarkSeen("box1", id)
|
||||
assert.NoError(t, err, "MarkSeen should succeed")
|
||||
err = sm.MarkSeen("seen-box", id)
|
||||
require.NoError(t, err, "MarkSeen should succeed")
|
||||
|
||||
// Verify test message seen.
|
||||
msg, err = sm.GetMessage("box1", id)
|
||||
msg, err = sm.GetMessage("seen-box", id)
|
||||
require.NoError(t, err, "GetMessage must succeed")
|
||||
require.NotNil(t, msg, "GetMessage must return a result")
|
||||
assert.True(t, msg.Seen, "msg should have been seen")
|
||||
@@ -405,17 +416,17 @@ func TestRemoveMessage(t *testing.T) {
|
||||
sm, _ := testStoreManager()
|
||||
|
||||
// Add test messages.
|
||||
id1 := addTestMessage(sm, "box1", "subject 1")
|
||||
id2 := addTestMessage(sm, "box1", "subject 2")
|
||||
id3 := addTestMessage(sm, "box1", "subject 3")
|
||||
got, err := sm.GetMetadata("box1")
|
||||
id1 := addTestMessage(sm, "rm-box", "subject 1")
|
||||
id2 := addTestMessage(sm, "rm-box", "subject 2")
|
||||
id3 := addTestMessage(sm, "rm-box", "subject 3")
|
||||
got, err := sm.GetMetadata("rm-box")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 3)
|
||||
|
||||
// Delete message 2 and verify.
|
||||
err = sm.RemoveMessage("box1", id2)
|
||||
assert.NoError(t, err)
|
||||
got, err = sm.GetMetadata("box1")
|
||||
err = sm.RemoveMessage("rm-box", id2)
|
||||
require.NoError(t, err)
|
||||
got, err = sm.GetMetadata("rm-box")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2, "Should be 2 messages remaining")
|
||||
|
||||
@@ -431,19 +442,19 @@ func TestPurgeMessages(t *testing.T) {
|
||||
sm, _ := testStoreManager()
|
||||
|
||||
// Add test messages.
|
||||
_ = addTestMessage(sm, "box1", "subject 1")
|
||||
_ = addTestMessage(sm, "box1", "subject 2")
|
||||
_ = addTestMessage(sm, "box1", "subject 3")
|
||||
got, err := sm.GetMetadata("box1")
|
||||
_ = addTestMessage(sm, "purge-box", "subject 1")
|
||||
_ = addTestMessage(sm, "purge-box", "subject 2")
|
||||
_ = addTestMessage(sm, "purge-box", "subject 3")
|
||||
got, err := sm.GetMetadata("purge-box")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 3)
|
||||
|
||||
// Purge and verify.
|
||||
err = sm.PurgeMessages("box1")
|
||||
assert.NoError(t, err)
|
||||
got, err = sm.GetMetadata("box1")
|
||||
err = sm.PurgeMessages("purge-box")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 0, "Purge should remove all mailbox messages")
|
||||
got, err = sm.GetMetadata("purge-box")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, got, "Purge should remove all mailbox messages")
|
||||
}
|
||||
|
||||
func TestSourceReader(t *testing.T) {
|
||||
@@ -490,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()
|
||||
@@ -543,7 +586,7 @@ func assertMessageCount(t *testing.T, sm *message.StoreManager, mailbox string,
|
||||
t.Helper()
|
||||
|
||||
metas, err := sm.GetMetadata(mailbox)
|
||||
assert.NoError(t, err, "StoreManager GetMetadata failed")
|
||||
require.NoError(t, err, "StoreManager GetMetadata failed")
|
||||
|
||||
got := len(metas)
|
||||
if got != count {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
"github.com/inbucket/inbucket/v3/pkg/storage"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"github.com/jhillyerd/enmime/v2"
|
||||
)
|
||||
|
||||
// Message holds both the metadata and content of a message.
|
||||
|
||||
@@ -140,7 +140,7 @@ func (hub *Hub) RemoveListener(l Listener) {
|
||||
// for unit tests.
|
||||
func (hub *Hub) Sync() {
|
||||
done := make(chan struct{})
|
||||
hub.opChan <- func(h *Hub) {
|
||||
hub.opChan <- func(_ *Hub) {
|
||||
close(done)
|
||||
}
|
||||
<-done
|
||||
|
||||
@@ -2,6 +2,7 @@ package msghub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
@@ -51,7 +52,7 @@ func (l *testListener) Receive(msg event.MessageMetadata) error {
|
||||
close(l.overflow)
|
||||
}
|
||||
if l.errorAfter > 0 && l.gotEvents > l.errorAfter {
|
||||
return fmt.Errorf("Too many messages")
|
||||
return errors.New("too many messages")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -80,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
|
||||
@@ -92,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
|
||||
@@ -177,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),
|
||||
}
|
||||
@@ -202,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 {
|
||||
@@ -221,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),
|
||||
@@ -252,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])
|
||||
@@ -270,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),
|
||||
}
|
||||
@@ -295,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 {
|
||||
@@ -325,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),
|
||||
@@ -342,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),
|
||||
|
||||
@@ -2,6 +2,7 @@ package policy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
@@ -37,7 +38,7 @@ func (a *Addressing) ExtractMailbox(address string) (string, error) {
|
||||
}
|
||||
|
||||
if a.Config.MailboxNaming != config.FullNaming {
|
||||
return "", fmt.Errorf("Unknown MailboxNaming value: %v", a.Config.MailboxNaming)
|
||||
return "", fmt.Errorf("unknown MailboxNaming value: %v", a.Config.MailboxNaming)
|
||||
}
|
||||
|
||||
if domain == "" {
|
||||
@@ -45,7 +46,7 @@ func (a *Addressing) ExtractMailbox(address string) (string, error) {
|
||||
}
|
||||
|
||||
if !ValidateDomainPart(domain) {
|
||||
return "", fmt.Errorf("Domain part %q in %q failed validation", domain, address)
|
||||
return "", fmt.Errorf("domain part %q in %q failed validation", domain, address)
|
||||
}
|
||||
|
||||
return local + "@" + domain, nil
|
||||
@@ -74,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
|
||||
@@ -136,7 +142,7 @@ func ParseEmailAddress(address string) (local string, domain string, err error)
|
||||
return "", "", err
|
||||
}
|
||||
if !ValidateDomainPart(domain) {
|
||||
return "", "", fmt.Errorf("Domain part validation failed")
|
||||
return "", "", errors.New("domain part validation failed")
|
||||
}
|
||||
return local, domain, nil
|
||||
}
|
||||
@@ -229,7 +235,7 @@ func extractDomainMailbox(address string) (string, error) {
|
||||
}
|
||||
|
||||
if !ValidateDomainPart(domain) {
|
||||
return "", fmt.Errorf("Domain part %q in %q failed validation", domain, address)
|
||||
return "", fmt.Errorf("domain part %q in %q failed validation", domain, address)
|
||||
}
|
||||
|
||||
return domain, nil
|
||||
@@ -240,26 +246,26 @@ func extractDomainMailbox(address string) (string, error) {
|
||||
// domain part is optional and not validated.
|
||||
func parseEmailAddress(address string) (local string, domain string, err error) {
|
||||
if address == "" {
|
||||
return "", "", fmt.Errorf("empty address")
|
||||
return "", "", errors.New("empty address")
|
||||
}
|
||||
if len(address) > 320 {
|
||||
return "", "", fmt.Errorf("address exceeds 320 characters")
|
||||
return "", "", errors.New("address exceeds 320 characters")
|
||||
}
|
||||
|
||||
// Remove forward-path routes.
|
||||
if address[0] == '@' {
|
||||
end := strings.IndexRune(address, ':')
|
||||
if end == -1 {
|
||||
return "", "", fmt.Errorf("missing terminating ':' in route specification")
|
||||
return "", "", errors.New("missing terminating ':' in route specification")
|
||||
}
|
||||
address = address[end+1:]
|
||||
if address == "" {
|
||||
return "", "", fmt.Errorf("Address empty after removing route specification")
|
||||
return "", "", errors.New("address empty after removing route specification")
|
||||
}
|
||||
}
|
||||
|
||||
if address[0] == '.' {
|
||||
return "", "", fmt.Errorf("address cannot start with a period")
|
||||
return "", "", errors.New("address cannot start with a period")
|
||||
}
|
||||
|
||||
// Loop over address parsing out local part.
|
||||
@@ -268,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'):
|
||||
@@ -285,7 +291,7 @@ LOOP:
|
||||
return
|
||||
}
|
||||
inCharQuote = false
|
||||
case bytes.IndexByte([]byte("!#$%&'*+-/=?^_`{|}~"), c) >= 0:
|
||||
case strings.IndexByte("!#$%&'*+-/=?^_`{|}~", c) >= 0:
|
||||
// These specials can be used unquoted.
|
||||
err = buf.WriteByte(c)
|
||||
if err != nil {
|
||||
@@ -296,7 +302,7 @@ LOOP:
|
||||
// A single period is OK.
|
||||
if prev == '.' {
|
||||
// Sequence of periods is not permitted.
|
||||
return "", "", fmt.Errorf("Sequence of periods is not permitted")
|
||||
return "", "", errors.New("sequence of periods is not permitted")
|
||||
}
|
||||
err = buf.WriteByte(c)
|
||||
if err != nil {
|
||||
@@ -306,19 +312,20 @@ LOOP:
|
||||
case c == '\\':
|
||||
inCharQuote = true
|
||||
case c == '"':
|
||||
if inCharQuote {
|
||||
switch {
|
||||
case inCharQuote:
|
||||
err = buf.WriteByte(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
inCharQuote = false
|
||||
} else if inStringQuote {
|
||||
case inStringQuote:
|
||||
inStringQuote = false
|
||||
} else {
|
||||
default:
|
||||
if i == 0 {
|
||||
inStringQuote = true
|
||||
} else {
|
||||
return "", "", fmt.Errorf("Quoted string can only begin at start of address")
|
||||
return "", "", errors.New("quoted string can only begin at start of address")
|
||||
}
|
||||
}
|
||||
case c == '@':
|
||||
@@ -331,16 +338,16 @@ LOOP:
|
||||
} else {
|
||||
// End of local-part.
|
||||
if i > 128 {
|
||||
return "", "", fmt.Errorf("Local part must not exceed 128 characters")
|
||||
return "", "", errors.New("local part must not exceed 128 characters")
|
||||
}
|
||||
if prev == '.' {
|
||||
return "", "", fmt.Errorf("Local part cannot end with a period")
|
||||
return "", "", errors.New("local part cannot end with a period")
|
||||
}
|
||||
domain = address[i+1:]
|
||||
break LOOP
|
||||
}
|
||||
case c > 127:
|
||||
return "", "", fmt.Errorf("Characters outside of US-ASCII range not permitted")
|
||||
return "", "", errors.New("characters outside of US-ASCII range not permitted")
|
||||
default:
|
||||
if inCharQuote || inStringQuote {
|
||||
err = buf.WriteByte(c)
|
||||
@@ -349,16 +356,16 @@ LOOP:
|
||||
}
|
||||
inCharQuote = false
|
||||
} else {
|
||||
return "", "", fmt.Errorf("Character %q must be quoted", c)
|
||||
return "", "", fmt.Errorf("character %q must be quoted", c)
|
||||
}
|
||||
}
|
||||
prev = c
|
||||
}
|
||||
if inCharQuote {
|
||||
return "", "", fmt.Errorf("Cannot end address with unterminated quoted-pair")
|
||||
return "", "", errors.New("cannot end address with unterminated quoted-pair")
|
||||
}
|
||||
if inStringQuote {
|
||||
return "", "", fmt.Errorf("Cannot end address with unterminated string quote")
|
||||
return "", "", errors.New("cannot end address with unterminated string quote")
|
||||
}
|
||||
return buf.String(), domain, nil
|
||||
}
|
||||
@@ -369,22 +376,22 @@ LOOP:
|
||||
// quoted according to RFC3696.
|
||||
func parseMailboxName(localPart string) (result string, err error) {
|
||||
if localPart == "" {
|
||||
return "", fmt.Errorf("Mailbox name cannot be empty")
|
||||
return "", errors.New("mailbox name cannot be empty")
|
||||
}
|
||||
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':
|
||||
case '0' <= c && c <= '9':
|
||||
case bytes.IndexByte([]byte("!#$%&'*+-=/?^_`.{|}~"), c) >= 0:
|
||||
case strings.IndexByte("!#$%&'*+-=/?^_`.{|}~", c) >= 0:
|
||||
default:
|
||||
invalid = append(invalid, c)
|
||||
}
|
||||
}
|
||||
if len(invalid) > 0 {
|
||||
return "", fmt.Errorf("Mailbox name contained invalid character(s): %q", invalid)
|
||||
return "", fmt.Errorf("mailbox name contained invalid character(s): %q", invalid)
|
||||
}
|
||||
if idx := strings.Index(result, "+"); idx > -1 {
|
||||
result = result[0:idx]
|
||||
|
||||
@@ -33,7 +33,6 @@ func TestShouldAcceptDomain(t *testing.T) {
|
||||
if got != tc.want {
|
||||
t.Errorf("Got %v for %q, want: %v", got, tc.domain, tc.want)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
// Test with default reject.
|
||||
@@ -60,7 +59,6 @@ func TestShouldAcceptDomain(t *testing.T) {
|
||||
if got != tc.want {
|
||||
t.Errorf("Got %v for %q, want: %v", got, tc.domain, tc.want)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -90,7 +88,6 @@ func TestShouldStoreDomain(t *testing.T) {
|
||||
if got != tc.want {
|
||||
t.Errorf("Got store %v for %q, want: %v", got, tc.domain, tc.want)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
// Test with storage disabled.
|
||||
@@ -117,7 +114,6 @@ func TestShouldStoreDomain(t *testing.T) {
|
||||
if got != tc.want {
|
||||
t.Errorf("Got store %v for %q, want: %v", got, tc.domain, tc.want)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -287,24 +283,18 @@ func TestExtractMailboxValid(t *testing.T) {
|
||||
for _, tc := range testTable {
|
||||
if result, err := localPolicy.ExtractMailbox(tc.input); err != nil {
|
||||
t.Errorf("Error while parsing with local naming %q: %v", tc.input, err)
|
||||
} else {
|
||||
if result != tc.local {
|
||||
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.local, result)
|
||||
}
|
||||
} else if result != tc.local {
|
||||
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.local, result)
|
||||
}
|
||||
if result, err := fullPolicy.ExtractMailbox(tc.input); err != nil {
|
||||
t.Errorf("Error while parsing with full naming %q: %v", tc.input, err)
|
||||
} else {
|
||||
if result != tc.full {
|
||||
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.full, result)
|
||||
}
|
||||
} else if result != tc.full {
|
||||
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.full, result)
|
||||
}
|
||||
if result, err := domainPolicy.ExtractMailbox(tc.input); tc.domain != "" && err != nil {
|
||||
t.Errorf("Error while parsing with domain naming %q: %v", tc.input, err)
|
||||
} else {
|
||||
if result != tc.domain {
|
||||
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.domain, result)
|
||||
}
|
||||
} else if result != tc.domain {
|
||||
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.domain, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,10 +321,8 @@ func TestExtractDomainMailboxValid(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if result, err := domainPolicy.ExtractMailbox(tc.input); tc.domain != "" && err != nil {
|
||||
t.Errorf("Error while parsing with domain naming %q: %v", tc.input, err)
|
||||
} else {
|
||||
if result != tc.domain {
|
||||
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.domain, result)
|
||||
}
|
||||
} else if result != tc.domain {
|
||||
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.domain, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func MailboxListV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (
|
||||
messages, err := ctx.Manager.GetMetadata(name)
|
||||
if err != nil {
|
||||
// This doesn't indicate empty, likely an IO error
|
||||
return fmt.Errorf("Failed to get messages for %v: %v", name, err)
|
||||
return fmt.Errorf("failed to get messages for %v: %v", name, err)
|
||||
}
|
||||
jmessages := make([]*model.JSONMessageHeaderV1, len(messages))
|
||||
for i, msg := range messages {
|
||||
@@ -108,7 +108,7 @@ func MailboxMarkSeenV1(w http.ResponseWriter, req *http.Request, ctx *web.Contex
|
||||
dec := json.NewDecoder(req.Body)
|
||||
dm := model.JSONMessageHeaderV1{}
|
||||
if err := dec.Decode(&dm); err != nil {
|
||||
return fmt.Errorf("Failed to decode JSON: %v", err)
|
||||
return fmt.Errorf("failed to decode JSON: %v", err)
|
||||
}
|
||||
if dm.Seen {
|
||||
err = ctx.Manager.MarkSeen(name, id)
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
"github.com/inbucket/inbucket/v3/pkg/message"
|
||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"github.com/jhillyerd/enmime/v2"
|
||||
)
|
||||
|
||||
func TestRestMailboxList(t *testing.T) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -41,33 +42,56 @@ func New(baseURL string, opts ...Option) (*Client, error) {
|
||||
}
|
||||
|
||||
// ListMailbox returns a list of messages for the requested mailbox
|
||||
func (c *Client) ListMailbox(name string) (headers []*MessageHeader, err error) {
|
||||
func (c *Client) ListMailbox(name string) ([]*MessageHeader, error) {
|
||||
return c.ListMailboxWithContext(context.Background(), name)
|
||||
}
|
||||
|
||||
// ListMailboxWithContext returns a list of messages for the requested mailbox
|
||||
func (c *Client) ListMailboxWithContext(ctx context.Context, name string) ([]*MessageHeader, error) {
|
||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name)
|
||||
err = c.doJSON("GET", uri, &headers)
|
||||
headers := make([]*MessageHeader, 0, 32)
|
||||
|
||||
err := c.doJSON(ctx, "GET", uri, &headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add Client ref to each MessageHeader for convenience funcs.
|
||||
for _, h := range headers {
|
||||
h.client = c
|
||||
}
|
||||
return
|
||||
|
||||
return headers, nil
|
||||
}
|
||||
|
||||
// GetMessage returns the message details given a mailbox name and message ID.
|
||||
func (c *Client) GetMessage(name, id string) (message *Message, err error) {
|
||||
return c.GetMessageWithContext(context.Background(), name, id)
|
||||
}
|
||||
|
||||
// GetMessageWithContext returns the message details given a mailbox name and message ID.
|
||||
func (c *Client) GetMessageWithContext(ctx context.Context, name, id string) (*Message, error) {
|
||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id
|
||||
err = c.doJSON("GET", uri, &message)
|
||||
var message Message
|
||||
|
||||
err := c.doJSON(ctx, "GET", uri, &message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message.client = c
|
||||
return
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
// MarkSeen marks the specified message as having been read.
|
||||
func (c *Client) MarkSeen(name, id string) error {
|
||||
return c.MarkSeenWithContext(context.Background(), name, id)
|
||||
}
|
||||
|
||||
// MarkSeenWithContext marks the specified message as having been read.
|
||||
func (c *Client) MarkSeenWithContext(ctx context.Context, name, id string) error {
|
||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id
|
||||
err := c.doJSON("PATCH", uri, nil)
|
||||
err := c.doJSON(ctx, "PATCH", uri, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -76,19 +100,25 @@ func (c *Client) MarkSeen(name, id string) error {
|
||||
|
||||
// GetMessageSource returns the message source given a mailbox name and message ID.
|
||||
func (c *Client) GetMessageSource(name, id string) (*bytes.Buffer, error) {
|
||||
return c.GetMessageSourceWithContext(context.Background(), name, id)
|
||||
}
|
||||
|
||||
// GetMessageSourceWithContext returns the message source given a mailbox name and message ID.
|
||||
func (c *Client) GetMessageSourceWithContext(ctx context.Context, name, id string) (*bytes.Buffer, error) {
|
||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id + "/source"
|
||||
resp, err := c.do("GET", uri, nil)
|
||||
resp, err := c.do(ctx, "GET", uri, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil,
|
||||
fmt.Errorf("Unexpected HTTP response status %v: %s", resp.StatusCode, resp.Status)
|
||||
fmt.Errorf("unexpected HTTP response status %v: %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_, err = buf.ReadFrom(resp.Body)
|
||||
return buf, err
|
||||
@@ -96,29 +126,43 @@ func (c *Client) GetMessageSource(name, id string) (*bytes.Buffer, error) {
|
||||
|
||||
// DeleteMessage deletes a single message given the mailbox name and message ID.
|
||||
func (c *Client) DeleteMessage(name, id string) error {
|
||||
return c.DeleteMessageWithContext(context.Background(), name, id)
|
||||
}
|
||||
|
||||
// DeleteMessageWithContext deletes a single message given the mailbox name and message ID.
|
||||
func (c *Client) DeleteMessageWithContext(ctx context.Context, name, id string) error {
|
||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id
|
||||
resp, err := c.do("DELETE", uri, nil)
|
||||
resp, err := c.do(ctx, "DELETE", uri, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("Unexpected HTTP response status %v: %s", resp.StatusCode, resp.Status)
|
||||
return fmt.Errorf("unexpected HTTP response status %v: %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PurgeMailbox deletes all messages in the given mailbox
|
||||
func (c *Client) PurgeMailbox(name string) error {
|
||||
return c.PurgeMailboxWithContext(context.Background(), name)
|
||||
}
|
||||
|
||||
// PurgeMailboxWithContext deletes all messages in the given mailbox
|
||||
func (c *Client) PurgeMailboxWithContext(ctx context.Context, name string) error {
|
||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name)
|
||||
resp, err := c.do("DELETE", uri, nil)
|
||||
resp, err := c.do(ctx, "DELETE", uri, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("Unexpected HTTP response status %v: %s", resp.StatusCode, resp.Status)
|
||||
return fmt.Errorf("unexpected HTTP response status %v: %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -130,17 +174,32 @@ type MessageHeader struct {
|
||||
|
||||
// GetMessage returns this message with content
|
||||
func (h *MessageHeader) GetMessage() (message *Message, err error) {
|
||||
return h.client.GetMessage(h.Mailbox, h.ID)
|
||||
return h.GetMessageWithContext(context.Background())
|
||||
}
|
||||
|
||||
// GetMessageWithContext returns this message with content
|
||||
func (h *MessageHeader) GetMessageWithContext(ctx context.Context) (message *Message, err error) {
|
||||
return h.client.GetMessageWithContext(ctx, h.Mailbox, h.ID)
|
||||
}
|
||||
|
||||
// GetSource returns the source for this message
|
||||
func (h *MessageHeader) GetSource() (*bytes.Buffer, error) {
|
||||
return h.client.GetMessageSource(h.Mailbox, h.ID)
|
||||
return h.GetSourceWithContext(context.Background())
|
||||
}
|
||||
|
||||
// GetSourceWithContext returns the source for this message
|
||||
func (h *MessageHeader) GetSourceWithContext(ctx context.Context) (*bytes.Buffer, error) {
|
||||
return h.client.GetMessageSourceWithContext(ctx, h.Mailbox, h.ID)
|
||||
}
|
||||
|
||||
// Delete deletes this message from the mailbox
|
||||
func (h *MessageHeader) Delete() error {
|
||||
return h.client.DeleteMessage(h.Mailbox, h.ID)
|
||||
return h.DeleteWithContext(context.Background())
|
||||
}
|
||||
|
||||
// DeleteWithContext deletes this message from the mailbox
|
||||
func (h *MessageHeader) DeleteWithContext(ctx context.Context) error {
|
||||
return h.client.DeleteMessageWithContext(ctx, h.Mailbox, h.ID)
|
||||
}
|
||||
|
||||
// Message represents an Inbucket message including content
|
||||
@@ -151,10 +210,20 @@ type Message struct {
|
||||
|
||||
// GetSource returns the source for this message
|
||||
func (m *Message) GetSource() (*bytes.Buffer, error) {
|
||||
return m.client.GetMessageSource(m.Mailbox, m.ID)
|
||||
return m.GetSourceWithContext(context.Background())
|
||||
}
|
||||
|
||||
// GetSourceWithContext returns the source for this message
|
||||
func (m *Message) GetSourceWithContext(ctx context.Context) (*bytes.Buffer, error) {
|
||||
return m.client.GetMessageSourceWithContext(ctx, m.Mailbox, m.ID)
|
||||
}
|
||||
|
||||
// Delete deletes this message from the mailbox
|
||||
func (m *Message) Delete() error {
|
||||
return m.client.DeleteMessage(m.Mailbox, m.ID)
|
||||
return m.DeleteWithContext(context.Background())
|
||||
}
|
||||
|
||||
// DeleteWithContext deletes this message from the mailbox
|
||||
func (m *Message) DeleteWithContext(ctx context.Context) error {
|
||||
return m.client.DeleteMessageWithContext(ctx, m.Mailbox, m.ID)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type options struct {
|
||||
|
||||
// Option can apply itself to the private options type.
|
||||
type Option interface {
|
||||
apply(*options)
|
||||
apply(opts *options)
|
||||
}
|
||||
|
||||
func getDefaultOptions() *options {
|
||||
|
||||
@@ -16,34 +16,42 @@ func Example() {
|
||||
baseURL, teardown := exampleSetup()
|
||||
defer teardown()
|
||||
|
||||
// Begin by creating a new client using the base URL of your Inbucket server, i.e.
|
||||
// `localhost:9000`.
|
||||
restClient, err := client.New(baseURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err := func() error {
|
||||
// Begin by creating a new client using the base URL of your Inbucket server, i.e.
|
||||
// `localhost:9000`.
|
||||
restClient, err := client.New(baseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get a slice of message headers for the mailbox named `user1`.
|
||||
headers, err := restClient.ListMailbox("user1")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, header := range headers {
|
||||
fmt.Printf("ID: %v, Subject: %v\n", header.ID, header.Subject)
|
||||
}
|
||||
// Get a slice of message headers for the mailbox named `user1`.
|
||||
headers, err := restClient.ListMailbox("user1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, header := range headers {
|
||||
fmt.Printf("ID: %v, Subject: %v\n", header.ID, header.Subject)
|
||||
}
|
||||
|
||||
// Get the content of the first message.
|
||||
message, err := headers[0].GetMessage()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("\nFrom: %v\n", message.From)
|
||||
fmt.Printf("Text body:\n%v", message.Body.Text)
|
||||
// Get the content of the first message.
|
||||
message, err := headers[0].GetMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("\nFrom: %v\n", message.From)
|
||||
fmt.Printf("Text body:\n%v", message.Body.Text)
|
||||
|
||||
// Delete the second message.
|
||||
err = headers[1].Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
// Delete the second message.
|
||||
err = headers[1].Delete()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Print(err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
|
||||
@@ -2,6 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -11,7 +12,7 @@ import (
|
||||
|
||||
// httpClient allows http.Client to be mocked for tests
|
||||
type httpClient interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// Generic REST restClient
|
||||
@@ -21,26 +22,24 @@ type restClient struct {
|
||||
}
|
||||
|
||||
// do performs an HTTP request with this client and returns the response.
|
||||
func (c *restClient) do(method, uri string, body []byte) (*http.Response, error) {
|
||||
rel, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := c.baseURL.ResolveReference(rel)
|
||||
func (c *restClient) do(ctx context.Context, method, uri string, body []byte) (*http.Response, error) {
|
||||
url := c.baseURL.JoinPath(uri)
|
||||
var r io.Reader
|
||||
if body != nil {
|
||||
r = bytes.NewReader(body)
|
||||
}
|
||||
req, err := http.NewRequest(method, url.String(), r)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url.String(), r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s for %q: %v", method, url, err)
|
||||
}
|
||||
|
||||
return c.client.Do(req)
|
||||
}
|
||||
|
||||
// doJSON performs an HTTP request with this client and marshalls the JSON response into v.
|
||||
func (c *restClient) doJSON(method string, uri string, v interface{}) error {
|
||||
resp, err := c.do(method, uri, nil)
|
||||
func (c *restClient) doJSON(ctx context.Context, method string, uri string, v interface{}) error {
|
||||
resp, err := c.do(ctx, method, uri, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,22 +2,33 @@ package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const baseURLStr = "http://test.local:8080"
|
||||
const baseURLPathStr = "http://test.local:8080/inbucket"
|
||||
|
||||
var baseURL *url.URL
|
||||
|
||||
var baseURLPath *url.URL
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
baseURL, err = url.Parse(baseURLStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
baseURLPath, err = url.Parse(baseURLPathStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type mockHTTPClient struct {
|
||||
@@ -51,32 +62,42 @@ func (m *mockHTTPClient) ReqBody() []byte {
|
||||
return body
|
||||
}
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
var want, got string
|
||||
mth := &mockHTTPClient{}
|
||||
c := &restClient{mth, baseURL}
|
||||
body := []byte("Test body")
|
||||
|
||||
_, err := c.do("POST", "/dopost", body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
func TestDoTable(t *testing.T) {
|
||||
tests := []struct {
|
||||
method string
|
||||
uri string
|
||||
wantMethod string
|
||||
base *url.URL
|
||||
wantURL string
|
||||
wantBody []byte
|
||||
}{
|
||||
{method: "GET", wantMethod: "GET", uri: "/doget", base: baseURL, wantURL: baseURLStr + "/doget", wantBody: []byte("Test body 1")},
|
||||
{method: "POST", wantMethod: "POST", uri: "/dopost", base: baseURL, wantURL: baseURLStr + "/dopost", wantBody: []byte("Test body 2")},
|
||||
{method: "GET", wantMethod: "GET", uri: "/doget", base: baseURLPath, wantURL: baseURLPathStr + "/doget", wantBody: []byte("Test body 3")},
|
||||
{method: "POST", wantMethod: "POST", uri: "/dopost", base: baseURLPath, wantURL: baseURLPathStr + "/dopost", wantBody: []byte("Test body 4")},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testname := fmt.Sprintf("%s,%s", test.method, test.wantURL)
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mth := &mockHTTPClient{}
|
||||
c := &restClient{mth, test.base}
|
||||
|
||||
want = "POST"
|
||||
got = mth.req.Method
|
||||
if got != want {
|
||||
t.Errorf("req.Method == %q, want %q", got, want)
|
||||
}
|
||||
resp, err := c.do(ctx, test.method, test.uri, test.wantBody)
|
||||
require.NoError(t, err)
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
want = baseURLStr + "/dopost"
|
||||
got = mth.req.URL.String()
|
||||
if got != want {
|
||||
t.Errorf("req.URL == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
b := mth.ReqBody()
|
||||
if !bytes.Equal(b, body) {
|
||||
t.Errorf("req.Body == %q, want %q", b, body)
|
||||
if mth.req.Method != test.wantMethod {
|
||||
t.Errorf("req.Method == %q, want %q", mth.req.Method, test.wantMethod)
|
||||
}
|
||||
if mth.req.URL.String() != test.wantURL {
|
||||
t.Errorf("req.URL == %q, want %q", mth.req.URL.String(), test.wantURL)
|
||||
}
|
||||
if !bytes.Equal(mth.ReqBody(), test.wantBody) {
|
||||
t.Errorf("req.Body == %q, want %q", mth.ReqBody(), test.wantBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +110,7 @@ func TestDoJSON(t *testing.T) {
|
||||
c := &restClient{mth, baseURL}
|
||||
|
||||
var v map[string]interface{}
|
||||
err := c.doJSON("GET", "/doget", &v)
|
||||
err := c.doJSON(context.Background(), "GET", "/doget", &v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -123,7 +144,7 @@ func TestDoJSONNilV(t *testing.T) {
|
||||
mth := &mockHTTPClient{}
|
||||
c := &restClient{mth, baseURL}
|
||||
|
||||
err := c.doJSON("GET", "/doget", nil)
|
||||
err := c.doJSON(context.Background(), "GET", "/doget", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package rest
|
||||
|
||||
import "github.com/gorilla/mux"
|
||||
import "github.com/inbucket/inbucket/v3/pkg/server/web"
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/inbucket/inbucket/v3/pkg/server/web"
|
||||
)
|
||||
|
||||
// SetupRoutes populates the routes for the REST interface
|
||||
func SetupRoutes(r *mux.Router) {
|
||||
|
||||
@@ -2,12 +2,14 @@ package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/config"
|
||||
"github.com/inbucket/inbucket/v3/pkg/message"
|
||||
@@ -16,24 +18,36 @@ import (
|
||||
)
|
||||
|
||||
func testRestGet(url string) (*httptest.ResponseRecorder, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
req.Header.Add("Accept", "application/json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Pass request to handlers directly.
|
||||
w := httptest.NewRecorder()
|
||||
web.Router.ServeHTTP(w, req)
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func testRestPatch(url string, body string) (*httptest.ResponseRecorder, error) {
|
||||
req, err := http.NewRequest("PATCH", url, strings.NewReader(body))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, url, strings.NewReader(body))
|
||||
req.Header.Add("Accept", "application/json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Pass request to handlers directly.
|
||||
w := httptest.NewRecorder()
|
||||
web.Router.ServeHTTP(w, req)
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
@@ -122,9 +136,10 @@ func getDecodedPath(o interface{}, path ...string) (interface{}, string) {
|
||||
if o == nil {
|
||||
return nil, " is nil"
|
||||
}
|
||||
key := path[0]
|
||||
present := false
|
||||
|
||||
var present bool
|
||||
var val interface{}
|
||||
key := path[0]
|
||||
if key[0] == '[' {
|
||||
// Expecting slice.
|
||||
index, err := strconv.Atoi(strings.Trim(key, "[]"))
|
||||
@@ -147,12 +162,15 @@ func getDecodedPath(o interface{}, path ...string) (interface{}, string) {
|
||||
}
|
||||
val, present = omap[key]
|
||||
}
|
||||
|
||||
if !present {
|
||||
return nil, "/" + key + " is missing"
|
||||
}
|
||||
|
||||
result, msg := getDecodedPath(val, path[1:]...)
|
||||
if msg != "" {
|
||||
return nil, "/" + key + msg
|
||||
}
|
||||
|
||||
return result, ""
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func FullAssembly(conf *config.Root) (*Services, error) {
|
||||
// Configure extensions.
|
||||
extHost := extension.NewHost()
|
||||
luaHost, err := luahost.New(conf.Lua, extHost)
|
||||
if err != nil {
|
||||
if err != nil && err != luahost.ErrNoScript {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -134,48 +135,46 @@ func (s *Server) startSession(id int, conn net.Conn) {
|
||||
line, err := ssn.readLine()
|
||||
ssn.logger.Debug().Msgf("read %s", line)
|
||||
if err == nil {
|
||||
if cmd, arg, ok := ssn.parseCmd(line); ok {
|
||||
// Check against valid SMTP commands
|
||||
if cmd == "" {
|
||||
ssn.send("-ERR Speak up")
|
||||
continue
|
||||
cmd, arg := ssn.parseCmd(line)
|
||||
// Commands we handle in any state
|
||||
if cmd == "CAPA" {
|
||||
// List our capabilities per RFC2449
|
||||
ssn.send("+OK Capability list follows")
|
||||
ssn.send("TOP")
|
||||
ssn.send("USER")
|
||||
ssn.send("UIDL")
|
||||
ssn.send("IMPLEMENTATION Inbucket")
|
||||
if s.tlsConfig != nil && s.tlsState == nil && !s.config.ForceTLS {
|
||||
ssn.send("STLS")
|
||||
}
|
||||
if !commands[cmd] {
|
||||
ssn.send(fmt.Sprintf("-ERR Syntax error, %v command unrecognized", cmd))
|
||||
ssn.logger.Warn().Msgf("Unrecognized command: %v", cmd)
|
||||
continue
|
||||
}
|
||||
|
||||
// Commands we handle in any state
|
||||
switch cmd {
|
||||
case "CAPA":
|
||||
// List our capabilities per RFC2449
|
||||
ssn.send("+OK Capability list follows")
|
||||
ssn.send("TOP")
|
||||
ssn.send("USER")
|
||||
ssn.send("UIDL")
|
||||
ssn.send("IMPLEMENTATION Inbucket")
|
||||
if s.tlsConfig != nil && s.tlsState == nil && !s.config.ForceTLS {
|
||||
ssn.send("STLS")
|
||||
}
|
||||
ssn.send(".")
|
||||
continue
|
||||
}
|
||||
|
||||
// Send command to handler for current state
|
||||
switch ssn.state {
|
||||
case AUTHORIZATION:
|
||||
ssn.authorizationHandler(cmd, arg)
|
||||
continue
|
||||
case TRANSACTION:
|
||||
ssn.transactionHandler(cmd, arg)
|
||||
continue
|
||||
}
|
||||
ssn.logger.Error().Msgf("Session entered unexpected state %v", ssn.state)
|
||||
break
|
||||
} else {
|
||||
ssn.send("-ERR Syntax error, command garbled")
|
||||
ssn.send(".")
|
||||
continue
|
||||
}
|
||||
|
||||
// Check against valid SMTP commands
|
||||
if cmd == "" {
|
||||
ssn.send("-ERR Speak up")
|
||||
continue
|
||||
}
|
||||
|
||||
if !commands[cmd] {
|
||||
ssn.send(fmt.Sprintf("-ERR Syntax error, %v command unrecognized", cmd))
|
||||
ssn.logger.Warn().Msgf("Unrecognized command: %v", cmd)
|
||||
continue
|
||||
}
|
||||
|
||||
// Send command to handler for current state
|
||||
switch ssn.state {
|
||||
case AUTHORIZATION:
|
||||
ssn.authorizationHandler(ctx, cmd, arg)
|
||||
continue
|
||||
case TRANSACTION:
|
||||
ssn.transactionHandler(cmd, arg)
|
||||
continue
|
||||
}
|
||||
|
||||
ssn.logger.Error().Msgf("Session entered unexpected state %v", ssn.state)
|
||||
break
|
||||
} else {
|
||||
// readLine() returned an error
|
||||
if err == io.EOF {
|
||||
@@ -208,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")
|
||||
@@ -216,24 +215,26 @@ 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")
|
||||
s.ooSeq(cmd)
|
||||
return
|
||||
}
|
||||
if s.tlsState != nil {
|
||||
// TLS state previously valid.
|
||||
s.logger.Debug().Msg("-ERR A TLS session already agreed upon.")
|
||||
s.send("-ERR A TLS session already agreed upon.")
|
||||
s.ooSeq(cmd)
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -463,7 +464,7 @@ func (s *Session) transactionHandler(cmd string, args []string) {
|
||||
s.processDeletes()
|
||||
s.enterState(QUIT)
|
||||
case "NOOP":
|
||||
s.send("+OK I have sucessfully done nothing")
|
||||
s.send("+OK I have successfully done nothing")
|
||||
case "RSET":
|
||||
// Reset session, don't actually delete anything I told you to
|
||||
s.logger.Debug().Msgf("Resetting session state on RSET request")
|
||||
@@ -631,14 +632,14 @@ func (s *Session) readLine() (line string, err error) {
|
||||
return line, nil
|
||||
}
|
||||
|
||||
func (s *Session) parseCmd(line string) (cmd string, args []string, ok bool) {
|
||||
func (s *Session) parseCmd(line string) (cmd string, args []string) {
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
if line == "" {
|
||||
return "", nil, true
|
||||
return "", nil
|
||||
}
|
||||
|
||||
words := strings.Split(line, " ")
|
||||
return strings.ToUpper(words[0]), words[1:], true
|
||||
return strings.ToUpper(words[0]), words[1:]
|
||||
}
|
||||
|
||||
func (s *Session) reset() {
|
||||
|
||||
@@ -29,8 +29,7 @@ func TestNoTLS(t *testing.T) {
|
||||
pipe := setupPOPSession(t, server)
|
||||
c := textproto.NewConn(pipe)
|
||||
defer func() {
|
||||
_ = c.PrintfLine("QUIT")
|
||||
_, _ = c.ReadLine()
|
||||
_ = c.Close()
|
||||
server.Drain()
|
||||
}()
|
||||
|
||||
@@ -41,21 +40,15 @@ func TestNoTLS(t *testing.T) {
|
||||
if !strings.HasPrefix(reply, "+OK") {
|
||||
t.Fatalf("Initial line is not +OK")
|
||||
}
|
||||
|
||||
// Verify CAPA response does not include STLS.
|
||||
if err := c.PrintfLine("CAPA"); err != nil {
|
||||
t.Fatalf("Failed to send CAPA; %v.", err)
|
||||
}
|
||||
replies := []string{}
|
||||
for {
|
||||
reply, err := c.ReadLine()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading CAPA line failed %v", err)
|
||||
}
|
||||
if reply == "." {
|
||||
break
|
||||
}
|
||||
replies = append(replies, reply)
|
||||
replies, err := c.ReadDotLines()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading CAPA line failed %v", err)
|
||||
}
|
||||
|
||||
for _, r := range replies {
|
||||
if r == "STLS" {
|
||||
t.Errorf("TLS not enabled but received STLS.")
|
||||
@@ -63,14 +56,44 @@ func TestNoTLS(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSTLSWithTLSDisabled(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
server := setupPOPServer(t, ds, false, false)
|
||||
pipe := setupPOPSession(t, server)
|
||||
_ = pipe.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
c := textproto.NewConn(pipe)
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
server.Drain()
|
||||
}()
|
||||
|
||||
reply, err := c.ReadLine()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading initial line failed %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(reply, "+OK") {
|
||||
t.Fatalf("Initial line is not +OK")
|
||||
}
|
||||
|
||||
if err := c.PrintfLine("STLS"); err != nil {
|
||||
t.Fatalf("Failed to send STLS; %v.", err)
|
||||
}
|
||||
reply, err = c.ReadLine()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading STLS reply line failed %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(reply, "-ERR") {
|
||||
t.Errorf("STLS should have errored: %s", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartTLS(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
server := setupPOPServer(t, ds, true, false)
|
||||
pipe := setupPOPSession(t, server)
|
||||
c := textproto.NewConn(pipe)
|
||||
defer func() {
|
||||
_ = c.PrintfLine("QUIT")
|
||||
_, _ = c.ReadLine()
|
||||
_ = c.Close()
|
||||
server.Drain()
|
||||
}()
|
||||
|
||||
@@ -81,28 +104,21 @@ func TestStartTLS(t *testing.T) {
|
||||
if !strings.HasPrefix(reply, "+OK") {
|
||||
t.Fatalf("Initial line is not +OK")
|
||||
}
|
||||
|
||||
// Verify CAPA response does not include STLS.
|
||||
if err := c.PrintfLine("CAPA"); err != nil {
|
||||
t.Fatalf("Failed to send CAPA; %v.", err)
|
||||
}
|
||||
replies := []string{}
|
||||
for {
|
||||
reply, err := c.ReadLine()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading CAPA line failed %v", err)
|
||||
}
|
||||
if reply == "." {
|
||||
break
|
||||
}
|
||||
replies = append(replies, reply)
|
||||
replies, err := c.ReadDotLines()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading CAPA line failed %v", err)
|
||||
}
|
||||
|
||||
sawTLS := false
|
||||
for _, r := range replies {
|
||||
if r == "STLS" {
|
||||
sawTLS = true
|
||||
}
|
||||
}
|
||||
|
||||
if !sawTLS {
|
||||
t.Errorf("TLS enabled but no STLS capability.")
|
||||
}
|
||||
@@ -138,15 +154,95 @@ func TestStartTLS(t *testing.T) {
|
||||
if !strings.HasPrefix(reply, "+OK") {
|
||||
t.Fatalf("CAPA failed: %s", reply)
|
||||
}
|
||||
for {
|
||||
reply, err := c.ReadLine()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading CAPA line failed %v", err)
|
||||
}
|
||||
if reply == "." {
|
||||
break
|
||||
_, err = c.ReadDotLines()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading CAPA line failed %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDupStartTLS(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
server := setupPOPServer(t, ds, true, false)
|
||||
pipe := setupPOPSession(t, server)
|
||||
_ = pipe.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
c := textproto.NewConn(pipe)
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
server.Drain()
|
||||
}()
|
||||
|
||||
reply, err := c.ReadLine()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading initial line failed %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(reply, "+OK") {
|
||||
t.Fatalf("Initial line is not +OK")
|
||||
}
|
||||
|
||||
// Verify CAPA response includes STLS.
|
||||
if err := c.PrintfLine("CAPA"); err != nil {
|
||||
t.Fatalf("Failed to send CAPA; %v.", err)
|
||||
}
|
||||
replies, err := c.ReadDotLines()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading CAPA line failed %v", err)
|
||||
}
|
||||
sawTLS := false
|
||||
for _, r := range replies {
|
||||
if r == "STLS" {
|
||||
sawTLS = true
|
||||
}
|
||||
}
|
||||
if !sawTLS {
|
||||
t.Errorf("TLS enabled but no STLS capability.")
|
||||
}
|
||||
|
||||
t.Log("Sending first STLS command, expected to succeed")
|
||||
if err := c.PrintfLine("STLS"); err != nil {
|
||||
t.Fatalf("Failed to send STLS; %v.", err)
|
||||
}
|
||||
reply, err = c.ReadLine()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading STLS reply line failed %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(reply, "+OK") {
|
||||
t.Fatalf("STLS failed: %s", reply)
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
tlsConn := tls.Client(pipe, tlsConfig)
|
||||
ctx, toCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer toCancel()
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
t.Fatalf("TLS handshake failed; %v", err)
|
||||
}
|
||||
c = textproto.NewConn(tlsConn)
|
||||
|
||||
t.Log("Sending second STLS command, expected to fail")
|
||||
if err := c.PrintfLine("STLS"); err != nil {
|
||||
t.Fatalf("Failed to send STLS; %v.", err)
|
||||
}
|
||||
reply, err = c.ReadLine()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading STLS reply line failed %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(reply, "-ERR") {
|
||||
t.Fatalf("STLS failed: %s", reply)
|
||||
}
|
||||
|
||||
// Send STAT to verify handler has not crashed.
|
||||
if err := c.PrintfLine("STAT"); err != nil {
|
||||
t.Fatalf("Failed to send STAT; %v.", err)
|
||||
}
|
||||
reply, err = c.ReadLine()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading STAT reply line failed %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(reply, "-ERR") {
|
||||
t.Fatalf("STAT failed: %s", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForceTLS(t *testing.T) {
|
||||
@@ -165,8 +261,7 @@ func TestForceTLS(t *testing.T) {
|
||||
}
|
||||
c := textproto.NewConn(tlsConn)
|
||||
defer func() {
|
||||
_ = c.PrintfLine("QUIT")
|
||||
_, _ = c.ReadLine()
|
||||
_ = c.Close()
|
||||
server.Drain()
|
||||
}()
|
||||
|
||||
@@ -178,27 +273,18 @@ func TestForceTLS(t *testing.T) {
|
||||
t.Fatalf("Initial line is not +OK")
|
||||
}
|
||||
|
||||
// Verify CAPA response does not include STLS.
|
||||
if err := c.PrintfLine("CAPA"); err != nil {
|
||||
t.Fatalf("Failed to send CAPA; %v.", err)
|
||||
}
|
||||
reply, err = c.ReadLine()
|
||||
replies, err := c.ReadDotLines()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading CAPA reply line failed %v", err)
|
||||
t.Fatalf("Reading CAPA line failed %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(reply, "+OK") {
|
||||
t.Fatalf("CAPA failed: %s", reply)
|
||||
}
|
||||
for {
|
||||
reply, err := c.ReadLine()
|
||||
if err != nil {
|
||||
t.Fatalf("Reading CAPA line failed %v", err)
|
||||
}
|
||||
if reply == "STLS" {
|
||||
for _, r := range replies {
|
||||
if r == "STLS" {
|
||||
t.Errorf("STLS in CAPA in forceTLS mode.")
|
||||
}
|
||||
if reply == "." {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,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,
|
||||
}
|
||||
@@ -258,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
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func NewServer(pop3Config config.POP3, store storage.Store) (*Server, error) {
|
||||
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(pop3Config.TLSCert, pop3Config.TLSPrivKey)
|
||||
if err != nil {
|
||||
slog.Error().Msgf("Failed loading X509 KeyPair: %v", err)
|
||||
return nil, fmt.Errorf("Failed to configure TLS; %v", err)
|
||||
return nil, fmt.Errorf("failed to configure TLS; %v", err)
|
||||
// Do not silently turn off Security.
|
||||
}
|
||||
slog.Debug().Msg("TLS config available")
|
||||
@@ -97,8 +97,8 @@ func (s *Server) serve(ctx context.Context) {
|
||||
} else {
|
||||
tempDelay *= 2
|
||||
}
|
||||
if max := 1 * time.Second; tempDelay > max {
|
||||
tempDelay = max
|
||||
if maxDelay := 1 * time.Second; tempDelay > maxDelay {
|
||||
tempDelay = maxDelay
|
||||
}
|
||||
log.Error().Str("module", "pop3").Err(err).
|
||||
Msgf("POP3 accept timout; retrying in %v", tempDelay)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -114,7 +116,11 @@ type Session struct {
|
||||
// NewSession creates a new Session for the given connection
|
||||
func NewSession(server *Server, id int, conn net.Conn, logger zerolog.Logger) *Session {
|
||||
reader := bufio.NewReader(conn)
|
||||
host, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
||||
|
||||
remoteHost := conn.RemoteAddr().String()
|
||||
if host, _, err := net.SplitHostPort(remoteHost); err == nil {
|
||||
remoteHost = host
|
||||
}
|
||||
|
||||
session := &Session{
|
||||
Server: server,
|
||||
@@ -122,7 +128,7 @@ func NewSession(server *Server, id int, conn net.Conn, logger zerolog.Logger) *S
|
||||
conn: conn,
|
||||
state: GREET,
|
||||
reader: reader,
|
||||
remoteHost: host,
|
||||
remoteHost: remoteHost,
|
||||
recipients: make([]*policy.Recipient, 0),
|
||||
logger: logger,
|
||||
debug: server.config.Debug,
|
||||
@@ -139,20 +145,23 @@ func (s *Session) String() string {
|
||||
return fmt.Sprintf("Session{id: %v, state: %v}", s.id, s.state)
|
||||
}
|
||||
|
||||
/* Session flow:
|
||||
* 1. Send initial greeting
|
||||
* 2. Receive cmd
|
||||
* 3. If good cmd, respond, optionally change state
|
||||
* 4. If bad cmd, respond error
|
||||
* 5. Goto 2
|
||||
*/
|
||||
// Session flow:
|
||||
// 1. Send initial greeting
|
||||
// 2. Receive cmd
|
||||
// 3. If good cmd, respond, optionally change state
|
||||
// 4. If bad cmd, respond error
|
||||
// 5. Goto 2
|
||||
func (s *Server) startSession(id int, conn net.Conn, logger zerolog.Logger) {
|
||||
logger = logger.Hook(logHook{}).With().
|
||||
Str("module", "smtp").
|
||||
Str("remote", conn.RemoteAddr().String()).
|
||||
Int("session", id).Logger()
|
||||
logger.Info().Msg("Starting SMTP session")
|
||||
|
||||
// Update WaitGroup and counters.
|
||||
s.wg.Add(1)
|
||||
expConnectsCurrent.Add(1)
|
||||
expConnectsTotal.Add(1)
|
||||
defer func() {
|
||||
if err := conn.Close(); err != nil {
|
||||
logger.Warn().Err(err).Msg("Closing connection")
|
||||
@@ -173,13 +182,13 @@ func (s *Server) startSession(id int, conn net.Conn, logger zerolog.Logger) {
|
||||
}
|
||||
line, err := ssn.readLine()
|
||||
if err == nil {
|
||||
//Handle LOGIN/PASSWORD states here, because they don't expect a command
|
||||
// Handle LOGIN/PASSWORD states here, because they don't expect a command.
|
||||
switch ssn.state {
|
||||
case LOGIN:
|
||||
ssn.loginHandler(line)
|
||||
ssn.loginHandler()
|
||||
continue
|
||||
case PASSWORD:
|
||||
ssn.passwordHandler(line)
|
||||
ssn.passwordHandler()
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -206,7 +215,7 @@ func (s *Server) startSession(id int, conn net.Conn, logger zerolog.Logger) {
|
||||
ssn.send("252 Cannot VRFY user, but will accept message")
|
||||
continue
|
||||
case "NOOP":
|
||||
ssn.send("250 I have sucessfully done nothing")
|
||||
ssn.send("250 I have successfully done nothing")
|
||||
continue
|
||||
case "RSET":
|
||||
// Reset session
|
||||
@@ -291,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))
|
||||
@@ -307,18 +316,18 @@ func parseHelloArgument(arg string) (string, error) {
|
||||
domain = arg[:idx]
|
||||
}
|
||||
if domain == "" {
|
||||
return "", fmt.Errorf("Invalid domain")
|
||||
return "", errors.New("invalid domain")
|
||||
}
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
func (s *Session) loginHandler(line string) {
|
||||
func (s *Session) loginHandler() {
|
||||
// Content and length of username is ignored.
|
||||
s.send(fmt.Sprintf("334 %v", passwordChallenge))
|
||||
s.enterState(PASSWORD)
|
||||
}
|
||||
|
||||
func (s *Session) passwordHandler(line string) {
|
||||
func (s *Session) passwordHandler() {
|
||||
// Content and length of password is ignored.
|
||||
s.send("235 Authentication successful")
|
||||
s.enterState(READY)
|
||||
@@ -327,8 +336,9 @@ func (s *Session) passwordHandler(line string) {
|
||||
// READY state -> waiting for MAIL
|
||||
// AUTH can change
|
||||
func (s *Session) readyHandler(cmd string, arg string) {
|
||||
if cmd == "STARTTLS" {
|
||||
if !s.Server.config.TLSEnabled {
|
||||
switch cmd {
|
||||
case "STARTTLS":
|
||||
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")
|
||||
@@ -344,122 +354,130 @@ 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)
|
||||
*s.tlsState = tlsConn.ConnectionState()
|
||||
s.enterState(GREET)
|
||||
} else if cmd == "AUTH" {
|
||||
|
||||
case "AUTH":
|
||||
args := strings.SplitN(arg, " ", 3)
|
||||
authMethod := args[0]
|
||||
switch authMethod {
|
||||
case "PLAIN":
|
||||
{
|
||||
if len(args) != 2 {
|
||||
s.send("500 Bad auth arguments")
|
||||
s.logger.Warn().Msgf("Bad auth attempt: %q", arg)
|
||||
return
|
||||
}
|
||||
s.logger.Info().Msgf("Accepting credentials: %q", args[1])
|
||||
s.send("235 2.7.0 Authentication successful")
|
||||
if len(args) != 2 {
|
||||
s.send("500 Bad auth arguments")
|
||||
s.logger.Warn().Msgf("Bad auth attempt: %q", arg)
|
||||
return
|
||||
}
|
||||
s.logger.Info().Msgf("Accepting credentials: %q", args[1])
|
||||
s.send("235 2.7.0 Authentication successful")
|
||||
return
|
||||
|
||||
case "LOGIN":
|
||||
{
|
||||
s.send(fmt.Sprintf("334 %v", usernameChallenge))
|
||||
s.enterState(LOGIN)
|
||||
return
|
||||
}
|
||||
s.send(fmt.Sprintf("334 %v", usernameChallenge))
|
||||
s.enterState(LOGIN)
|
||||
return
|
||||
|
||||
default:
|
||||
{
|
||||
s.send(fmt.Sprintf("500 Unsupported AUTH method: %v", authMethod))
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if cmd == "MAIL" {
|
||||
// Capture group 1: from address. 2: optional params.
|
||||
m := fromRegex.FindStringSubmatch(arg)
|
||||
if m == nil {
|
||||
s.send("501 Was expecting MAIL arg syntax of FROM:<address>")
|
||||
s.logger.Warn().Msgf("Bad MAIL argument: %q", arg)
|
||||
s.send(fmt.Sprintf("500 Unsupported AUTH method: %v", authMethod))
|
||||
return
|
||||
}
|
||||
from := m[1]
|
||||
s.logger.Debug().Msgf("Mail sender is %v", from)
|
||||
localpart, 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"
|
||||
}
|
||||
case "MAIL":
|
||||
s.parseMailFromCmd(arg)
|
||||
|
||||
// This is where the client may put BODY=8BITMIME, but we already
|
||||
// read the DATA as bytes, so it does not effect our processing.
|
||||
if m[2] != "" {
|
||||
args, ok := s.parseArgs(m[2])
|
||||
if !ok {
|
||||
s.send("501 Unable to parse MAIL ESMTP parameters")
|
||||
s.logger.Warn().Msgf("Bad MAIL argument: %q", arg)
|
||||
return
|
||||
}
|
||||
if args["SIZE"] != "" {
|
||||
size, err := strconv.ParseInt(args["SIZE"], 10, 32)
|
||||
if err != nil {
|
||||
s.send("501 Unable to parse SIZE as an integer")
|
||||
s.logger.Warn().Msgf("Unable to parse SIZE %q as an integer", args["SIZE"])
|
||||
return
|
||||
}
|
||||
if int(size) > s.config.MaxMessageBytes {
|
||||
s.send("552 Max message size exceeded")
|
||||
s.logger.Warn().Msgf("Client wanted to send oversized message: %v", args["SIZE"])
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process through extensions.
|
||||
extResult := s.extHost.Events.BeforeMailAccepted.Emit(
|
||||
&event.AddressParts{Local: localpart, Domain: domain})
|
||||
|
||||
if extResult == nil || *extResult {
|
||||
// Permitted by extension, or none had an opinion.
|
||||
origin, err := s.addrPolicy.ParseOrigin(from)
|
||||
if err != nil {
|
||||
s.send("501 Bad origin address syntax")
|
||||
s.logger.Warn().Str("from", from).Err(err).Msg("Bad address as MAIL arg")
|
||||
return
|
||||
}
|
||||
s.from = origin
|
||||
if !s.from.ShouldAccept() {
|
||||
s.send("501 Unauthorized domain")
|
||||
s.logger.Warn().Msgf("Bad domain sender %s", domain)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Info().Msgf("Mail from: %v", from)
|
||||
s.send(fmt.Sprintf("250 Roger, accepting mail from <%v>", from))
|
||||
s.enterState(MAIL)
|
||||
} else {
|
||||
s.send("550 Mail denied by policy")
|
||||
s.logger.Warn().Msgf("Extension denied mail from <%v>", from)
|
||||
return
|
||||
}
|
||||
} else if cmd == "EHLO" {
|
||||
case "EHLO":
|
||||
// Reset session
|
||||
s.logger.Debug().Msgf("Resetting session state on EHLO request")
|
||||
s.reset()
|
||||
s.send("250 Session reset")
|
||||
} else {
|
||||
|
||||
default:
|
||||
s.ooSeq(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// Parses `MAIL FROM` command.
|
||||
func (s *Session) parseMailFromCmd(arg string) {
|
||||
// Capture group 1: from address. 2: optional params.
|
||||
m := fromRegex.FindStringSubmatch(arg)
|
||||
if m == nil {
|
||||
s.send("501 Was expecting MAIL arg syntax of FROM:<address>")
|
||||
s.logger.Warn().Msgf("Bad MAIL argument: %q", arg)
|
||||
return
|
||||
}
|
||||
from := m[1]
|
||||
s.logger.Debug().Msgf("Mail sender is %v", from)
|
||||
|
||||
// Parse ESMTP parameters.
|
||||
if m[2] != "" {
|
||||
// Here the client may put BODY=8BITMIME, but Inbucket already
|
||||
// reads the DATA as bytes, so it does not effect mail processing.
|
||||
args, ok := s.parseArgs(m[2])
|
||||
if !ok {
|
||||
s.send("501 Unable to parse MAIL ESMTP parameters")
|
||||
s.logger.Warn().Msgf("Bad MAIL argument: %q", arg)
|
||||
return
|
||||
}
|
||||
|
||||
// Reject oversized messages.
|
||||
if args["SIZE"] != "" {
|
||||
size, err := strconv.ParseInt(args["SIZE"], 10, 32)
|
||||
if err != nil {
|
||||
s.send("501 Unable to parse SIZE as an integer")
|
||||
s.logger.Warn().Msgf("Unable to parse SIZE %q as an integer", args["SIZE"])
|
||||
return
|
||||
}
|
||||
if int(size) > s.config.MaxMessageBytes {
|
||||
s.send("552 Max message size exceeded")
|
||||
s.logger.Warn().Msgf("Client wanted to send oversized message: %v", args["SIZE"])
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse origin (from) address.
|
||||
origin, err := s.addrPolicy.ParseOrigin(from)
|
||||
if err != nil {
|
||||
s.send("501 Bad origin address syntax")
|
||||
s.logger.Warn().Str("from", from).Err(err).Msg("Bad address as MAIL arg")
|
||||
return
|
||||
}
|
||||
|
||||
// Add from to extSession for inspection.
|
||||
extSession := s.extSession()
|
||||
addrCopy := origin.Address
|
||||
extSession.From = &addrCopy
|
||||
|
||||
// Process through extensions.
|
||||
extAction := event.ActionDefer
|
||||
extResult := s.extHost.Events.BeforeMailFromAccepted.Emit(extSession)
|
||||
if extResult != nil {
|
||||
extAction = extResult.Action
|
||||
}
|
||||
if extAction == event.ActionDeny {
|
||||
s.send(fmt.Sprintf("%03d %s", extResult.ErrorCode, extResult.ErrorMsg))
|
||||
s.logger.Warn().Msgf("Extension denied mail from <%v>", from)
|
||||
return
|
||||
}
|
||||
|
||||
// Sender was permitted by an extension, or no extension rejected it.
|
||||
s.from = origin
|
||||
// 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", origin.Domain)
|
||||
return
|
||||
}
|
||||
|
||||
// Ok to transition to MAIL state.
|
||||
s.logger.Info().Msgf("Mail from: %v", from)
|
||||
s.send(fmt.Sprintf("250 Roger, accepting mail from <%v>", from))
|
||||
s.enterState(MAIL)
|
||||
}
|
||||
|
||||
// MAIL state -> waiting for RCPTs followed by DATA
|
||||
func (s *Session) mailHandler(cmd string, arg string) {
|
||||
switch cmd {
|
||||
@@ -476,7 +494,26 @@ func (s *Session) mailHandler(cmd string, arg string) {
|
||||
s.logger.Warn().Str("to", addr).Err(err).Msg("Bad address as RCPT arg")
|
||||
return
|
||||
}
|
||||
if !recip.ShouldAccept() {
|
||||
|
||||
// Append new address to extSession for inspection.
|
||||
addrCopy := recip.Address
|
||||
extSession := s.extSession()
|
||||
extSession.To = append(extSession.To, &addrCopy)
|
||||
|
||||
// Process through extensions.
|
||||
extAction := event.ActionDefer
|
||||
extResult := s.extHost.Events.BeforeRcptToAccepted.Emit(extSession)
|
||||
if extResult != nil {
|
||||
extAction = extResult.Action
|
||||
}
|
||||
if extAction == event.ActionDeny {
|
||||
s.send(fmt.Sprintf("%03d %s", extResult.ErrorCode, extResult.ErrorMsg))
|
||||
s.logger.Warn().Msgf("Extension denied mail to <%v>", recip.Address)
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore ShouldAccept if extensions explicitly allowed this Recipient.
|
||||
if extAction == event.ActionDefer && !recip.ShouldAccept() {
|
||||
s.logger.Warn().Str("to", addr).Msg("Rejecting recipient domain")
|
||||
s.send("550 Relay not permitted")
|
||||
return
|
||||
@@ -668,3 +705,23 @@ func (s *Session) ooSeq(cmd string) {
|
||||
s.send(fmt.Sprintf("503 Command %v is out of sequence", cmd))
|
||||
s.logger.Warn().Msgf("Wasn't expecting %v here", cmd)
|
||||
}
|
||||
|
||||
// extSession builds an SMTPSession for extensions.
|
||||
func (s *Session) extSession() *event.SMTPSession {
|
||||
var from *mail.Address
|
||||
if s.from != nil {
|
||||
addr := s.from.Address
|
||||
from = &addr
|
||||
}
|
||||
to := make([]*mail.Address, 0, len(s.recipients))
|
||||
for _, recip := range s.recipients {
|
||||
addr := recip.Address
|
||||
to = append(to, &addr)
|
||||
}
|
||||
|
||||
return &event.SMTPSession{
|
||||
From: from,
|
||||
To: to,
|
||||
RemoteAddr: s.remoteHost,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type scriptStep struct {
|
||||
@@ -45,13 +46,10 @@ func TestGreetStateValidCommands(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.send, func(t *testing.T) {
|
||||
defer server.Drain() // Required to prevent test logging data race.
|
||||
script := []scriptStep{
|
||||
tc,
|
||||
{"QUIT", 221}}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -60,7 +58,6 @@ func TestGreetStateValidCommands(t *testing.T) {
|
||||
func TestGreetState(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
server := setupSMTPServer(ds, extension.NewHost())
|
||||
defer server.Drain() // Required to prevent test logging data race.
|
||||
|
||||
tests := []scriptStep{
|
||||
{"HELO", 501},
|
||||
@@ -73,46 +70,42 @@ func TestGreetState(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.send, func(t *testing.T) {
|
||||
defer server.Drain() // Required to prevent test logging data race.
|
||||
script := []scriptStep{
|
||||
tc,
|
||||
{"QUIT", 221}}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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())
|
||||
defer server.Drain()
|
||||
|
||||
// Test out some empty envelope without blanks
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<>", 501},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
{"MAIL FROM:<>", 250},
|
||||
}
|
||||
playSession(t, server, script)
|
||||
|
||||
// Test out some empty envelope with blanks
|
||||
script = []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM: <>", 501},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
{"MAIL FROM: <>", 250},
|
||||
}
|
||||
playSession(t, server, script)
|
||||
}
|
||||
|
||||
// Test AUTH commands.
|
||||
func TestAuth(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
server := setupSMTPServer(ds, extension.NewHost())
|
||||
defer server.Drain()
|
||||
|
||||
// PLAIN AUTH
|
||||
script := []scriptStep{
|
||||
@@ -125,9 +118,7 @@ func TestAuth(t *testing.T) {
|
||||
{"RSET", 250},
|
||||
{"AUTH PLAIN aW5idWNrZXQ6cG Fzc3dvcmQK", 500},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
|
||||
// LOGIN AUTH
|
||||
script = []scriptStep{
|
||||
@@ -140,16 +131,13 @@ func TestAuth(t *testing.T) {
|
||||
{"", 334},
|
||||
{"", 235},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
}
|
||||
|
||||
// Test TLS commands.
|
||||
func TestTLS(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
server := setupSMTPServer(ds, extension.NewHost())
|
||||
defer server.Drain()
|
||||
|
||||
// Test Start TLS parsing.
|
||||
script := []scriptStep{
|
||||
@@ -157,9 +145,7 @@ func TestTLS(t *testing.T) {
|
||||
{"STARTTLS", 454}, // TLS unconfigured.
|
||||
}
|
||||
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
}
|
||||
|
||||
// Test valid commands in READY state.
|
||||
@@ -186,14 +172,11 @@ func TestReadyStateValidCommands(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.send, func(t *testing.T) {
|
||||
defer server.Drain()
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
tc,
|
||||
{"QUIT", 221}}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -212,17 +195,13 @@ func TestReadyStateRejectedDomains(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.send, func(t *testing.T) {
|
||||
defer server.Drain()
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
tc,
|
||||
{"QUIT", 221}}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test invalid commands in READY state.
|
||||
@@ -245,24 +224,19 @@ func TestReadyStateInvalidCommands(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.send, func(t *testing.T) {
|
||||
defer server.Drain()
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
tc,
|
||||
{"QUIT", 221}}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test commands in MAIL state
|
||||
func TestMailState(t *testing.T) {
|
||||
mds := test.NewStore()
|
||||
server := setupSMTPServer(mds, extension.NewHost())
|
||||
defer server.Drain()
|
||||
|
||||
// Test out some mangled READY commands
|
||||
script := []scriptStep{
|
||||
@@ -278,9 +252,7 @@ func TestMailState(t *testing.T) {
|
||||
{"RCPT TO:<first last@host.com>", 501},
|
||||
{"RCPT TO:<fred@fish@host.com", 501},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
|
||||
// Test out some good RCPT commands
|
||||
script = []scriptStep{
|
||||
@@ -297,9 +269,7 @@ func TestMailState(t *testing.T) {
|
||||
{"RCPT TO:<u1@[127.0.0.1]>", 250},
|
||||
{"RCPT TO:<u1@[IPv6:2001:db8:aaaa:1::100]>", 250},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
|
||||
// Test out recipient limit
|
||||
script = []scriptStep{
|
||||
@@ -312,9 +282,7 @@ func TestMailState(t *testing.T) {
|
||||
{"RCPT TO:<u5@gmail.com>", 250},
|
||||
{"RCPT TO:<u6@gmail.com>", 552},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
|
||||
// Test DATA
|
||||
script = []scriptStep{
|
||||
@@ -324,9 +292,7 @@ func TestMailState(t *testing.T) {
|
||||
{"DATA", 354},
|
||||
{".", 250},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
|
||||
// Test late EHLO, similar to RSET
|
||||
script = []scriptStep{
|
||||
@@ -337,9 +303,7 @@ func TestMailState(t *testing.T) {
|
||||
{"EHLO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
|
||||
// Test RSET
|
||||
script = []scriptStep{
|
||||
@@ -349,9 +313,7 @@ func TestMailState(t *testing.T) {
|
||||
{"RSET", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
|
||||
// Test QUIT
|
||||
script = []scriptStep{
|
||||
@@ -360,16 +322,13 @@ func TestMailState(t *testing.T) {
|
||||
{"RCPT TO:<u1@gmail.com>", 250},
|
||||
{"QUIT", 221},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playSession(t, server, script)
|
||||
}
|
||||
|
||||
// Test commands in DATA state
|
||||
func TestDataState(t *testing.T) {
|
||||
mds := test.NewStore()
|
||||
server := setupSMTPServer(mds, extension.NewHost())
|
||||
defer server.Drain()
|
||||
|
||||
var script []scriptStep
|
||||
pipe := setupSMTPSession(t, server)
|
||||
@@ -384,9 +343,7 @@ func TestDataState(t *testing.T) {
|
||||
{"RCPT TO:<u1@gmail.com>", 250},
|
||||
{"DATA", 354},
|
||||
}
|
||||
if err := playScriptAgainst(t, c, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playScriptAgainst(t, c, script)
|
||||
|
||||
// Send a message
|
||||
body := `To: u1@gmail.com
|
||||
@@ -416,9 +373,7 @@ Hi!
|
||||
{"RCPT TO:<u1@gmail.com>", 250},
|
||||
{"DATA", 354},
|
||||
}
|
||||
if err := playScriptAgainst(t, c, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
playScriptAgainst(t, c, script)
|
||||
|
||||
// Send a message
|
||||
body = `X-Useless-Header: true
|
||||
@@ -435,31 +390,282 @@ Hi!
|
||||
_, _, _ = c.ReadCodeLine(221)
|
||||
}
|
||||
|
||||
// Tests "MAIL FROM" emits BeforeMailFromAccepted event.
|
||||
func TestBeforeMailFromAcceptedEventEmitted(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
extHost := extension.NewHost()
|
||||
server := setupSMTPServer(ds, extHost)
|
||||
|
||||
var got *event.SMTPSession
|
||||
extHost.Events.BeforeMailFromAccepted.AddListener(
|
||||
"test",
|
||||
func(session event.SMTPSession) *event.SMTPResponse {
|
||||
got = &session
|
||||
return &event.SMTPResponse{Action: event.ActionDefer}
|
||||
})
|
||||
|
||||
// Play and verify SMTP session.
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"QUIT", 221}}
|
||||
playSession(t, server, script)
|
||||
|
||||
assert.NotNil(t, got, "BeforeMailListener did not receive Address")
|
||||
assert.Equal(t, "john@gmail.com", got.From.Address, "Address had wrong value")
|
||||
assert.Equal(t, "pipe", got.RemoteAddr, "RemoteAddr had wrong value")
|
||||
}
|
||||
|
||||
// Test "MAIL FROM" acts on BeforeMailFromAccepted event result.
|
||||
func TestBeforeMailFromAcceptedEventResponse(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
extHost := extension.NewHost()
|
||||
server := setupSMTPServer(ds, extHost)
|
||||
|
||||
var shouldReturn *event.SMTPResponse
|
||||
var gotEvent *event.SMTPSession
|
||||
|
||||
extHost.Events.BeforeMailFromAccepted.AddListener(
|
||||
"test",
|
||||
func(session event.SMTPSession) *event.SMTPResponse {
|
||||
gotEvent = &session
|
||||
return shouldReturn
|
||||
})
|
||||
|
||||
tcs := map[string]struct {
|
||||
script scriptStep // Command to send and SMTP code expected.
|
||||
eventRes event.SMTPResponse // Response to send from event listener.
|
||||
}{
|
||||
"allow": {
|
||||
script: scriptStep{"MAIL FROM:<john@gmail.com>", 250},
|
||||
eventRes: event.SMTPResponse{Action: event.ActionAllow},
|
||||
},
|
||||
"deny": {
|
||||
script: scriptStep{"MAIL FROM:<john@gmail.com>", 550},
|
||||
eventRes: event.SMTPResponse{
|
||||
Action: event.ActionDeny,
|
||||
ErrorCode: 550,
|
||||
ErrorMsg: "meh",
|
||||
},
|
||||
},
|
||||
"defer": {
|
||||
script: scriptStep{"MAIL FROM:<john@gmail.com>", 250},
|
||||
eventRes: event.SMTPResponse{Action: event.ActionDefer},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tcs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Reset event listener.
|
||||
shouldReturn = &tc.eventRes
|
||||
gotEvent = nil
|
||||
|
||||
// Play and verify SMTP session.
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
tc.script, // error code is the significant part.
|
||||
{"QUIT", 221}}
|
||||
playSession(t, server, script)
|
||||
|
||||
assert.NotNil(t, gotEvent, "BeforeMailFromAccepted did not receive event")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests "RCPT TO" emits BeforeRcptToAccepted event.
|
||||
func TestBeforeRcptToAcceptedSingleEventEmitted(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
extHost := extension.NewHost()
|
||||
server := setupSMTPServer(ds, extHost)
|
||||
|
||||
var got *event.SMTPSession
|
||||
extHost.Events.BeforeRcptToAccepted.AddListener(
|
||||
"test",
|
||||
func(session event.SMTPSession) *event.SMTPResponse {
|
||||
got = &session
|
||||
return &event.SMTPResponse{Action: event.ActionDefer}
|
||||
})
|
||||
|
||||
// Play and verify SMTP session.
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"RCPT TO:<user@gmail.com>", 250},
|
||||
{"QUIT", 221}}
|
||||
playSession(t, server, script)
|
||||
|
||||
require.NotNil(t, got, "BeforeRcptToListener did not receive SMTPSession")
|
||||
require.NotNil(t, got.From)
|
||||
require.NotNil(t, got.To)
|
||||
assert.Equal(t, "pipe", got.RemoteAddr, "RemoteAddr had wrong value")
|
||||
assert.Equal(t, "john@gmail.com", got.From.Address)
|
||||
assert.Len(t, got.To, 1)
|
||||
assert.Equal(t, "user@gmail.com", got.To[0].Address)
|
||||
}
|
||||
|
||||
// Tests "RCPT TO" emits many BeforeRcptToAccepted events.
|
||||
func TestBeforeRcptToAcceptedManyEventsEmitted(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
extHost := extension.NewHost()
|
||||
server := setupSMTPServer(ds, extHost)
|
||||
|
||||
var called int
|
||||
var got *event.SMTPSession
|
||||
extHost.Events.BeforeRcptToAccepted.AddListener(
|
||||
"test",
|
||||
func(session event.SMTPSession) *event.SMTPResponse {
|
||||
called++
|
||||
got = &session
|
||||
return &event.SMTPResponse{Action: event.ActionDefer}
|
||||
})
|
||||
|
||||
// Play and verify SMTP session.
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"RCPT TO:<user@gmail.com>", 250},
|
||||
{"RCPT TO:<user2@gmail.com>", 250},
|
||||
{"QUIT", 221}}
|
||||
playSession(t, server, script)
|
||||
|
||||
require.Equal(t, 2, called, "2 events should have been emitted")
|
||||
require.NotNil(t, got, "BeforeRcptToListener did not receive SMTPSession")
|
||||
require.NotNil(t, got.From)
|
||||
require.NotNil(t, got.To)
|
||||
assert.Equal(t, "john@gmail.com", got.From.Address)
|
||||
assert.Len(t, got.To, 2)
|
||||
assert.Equal(t, "user@gmail.com", got.To[0].Address)
|
||||
assert.Equal(t, "user2@gmail.com", got.To[1].Address)
|
||||
}
|
||||
|
||||
// Tests we can continue after denying a "RCPT TO".
|
||||
func TestBeforeRcptToAcceptedEventDeny(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
extHost := extension.NewHost()
|
||||
server := setupSMTPServer(ds, extHost)
|
||||
|
||||
var called int
|
||||
var got *event.SMTPSession
|
||||
extHost.Events.BeforeRcptToAccepted.AddListener(
|
||||
"test",
|
||||
func(session event.SMTPSession) *event.SMTPResponse {
|
||||
called++
|
||||
|
||||
// Reject bad address.
|
||||
action := event.ActionDefer
|
||||
for _, to := range session.To {
|
||||
if to.Address == "bad@apple.com" {
|
||||
action = event.ActionDeny
|
||||
}
|
||||
}
|
||||
|
||||
got = &session
|
||||
return &event.SMTPResponse{Action: action, ErrorCode: 550, ErrorMsg: "rotten"}
|
||||
})
|
||||
|
||||
// Play and verify SMTP session.
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"RCPT TO:<user@gmail.com>", 250},
|
||||
{"RCPT TO:<bad@apple.com>", 550},
|
||||
{"RCPT TO:<user2@gmail.com>", 250},
|
||||
{"QUIT", 221}}
|
||||
playSession(t, server, script)
|
||||
|
||||
require.Equal(t, 3, called, "3 events should have been emitted")
|
||||
require.NotNil(t, got, "BeforeRcptToListener did not receive SMTPSession")
|
||||
require.NotNil(t, got.From)
|
||||
require.NotNil(t, got.To)
|
||||
assert.Equal(t, "john@gmail.com", got.From.Address)
|
||||
|
||||
// Verify bad apple dropped from final event.
|
||||
assert.Len(t, got.To, 2)
|
||||
assert.Equal(t, "user@gmail.com", got.To[0].Address)
|
||||
assert.Equal(t, "user2@gmail.com", got.To[1].Address)
|
||||
}
|
||||
|
||||
// Test "RCPT TO" acts on BeforeRcptToAccepted event result.
|
||||
func TestBeforeRcptToAcceptedEventResponse(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
extHost := extension.NewHost()
|
||||
server := setupSMTPServer(ds, extHost)
|
||||
|
||||
var shouldReturn *event.SMTPResponse
|
||||
var gotEvent *event.SMTPSession
|
||||
extHost.Events.BeforeRcptToAccepted.AddListener(
|
||||
"test",
|
||||
func(session event.SMTPSession) *event.SMTPResponse {
|
||||
gotEvent = &session
|
||||
return shouldReturn
|
||||
})
|
||||
|
||||
tcs := map[string]struct {
|
||||
script scriptStep // Command to send and SMTP code expected.
|
||||
eventRes event.SMTPResponse // Response to send from event listener.
|
||||
}{
|
||||
"allow": {
|
||||
script: scriptStep{"RCPT TO:<john@gmail.com>", 250},
|
||||
eventRes: event.SMTPResponse{Action: event.ActionAllow},
|
||||
},
|
||||
"deny": {
|
||||
script: scriptStep{"RCPT TO:<john@gmail.com>", 550},
|
||||
eventRes: event.SMTPResponse{
|
||||
Action: event.ActionDeny,
|
||||
ErrorCode: 550,
|
||||
ErrorMsg: "meh",
|
||||
},
|
||||
},
|
||||
"defer": {
|
||||
script: scriptStep{"RCPT TO:<john@gmail.com>", 250},
|
||||
eventRes: event.SMTPResponse{Action: event.ActionDefer},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tcs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Reset event listener.
|
||||
shouldReturn = &tc.eventRes
|
||||
gotEvent = nil
|
||||
|
||||
// Play and verify SMTP session.
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<user@gmail.com>", 250},
|
||||
tc.script, // error code is the significant part.
|
||||
{"QUIT", 221}}
|
||||
playSession(t, server, script)
|
||||
|
||||
assert.NotNil(t, gotEvent, "BeforeRcptToListener did not receive SMTPSession")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// playSession creates a new session, reads the greeting and then plays the script
|
||||
func playSession(t *testing.T, server *Server, script []scriptStep) error {
|
||||
func playSession(t *testing.T, server *Server, script []scriptStep) {
|
||||
t.Helper()
|
||||
pipe := setupSMTPSession(t, server)
|
||||
c := textproto.NewConn(pipe)
|
||||
|
||||
if code, _, err := c.ReadCodeLine(220); err != nil {
|
||||
return fmt.Errorf("Expected a 220 greeting, got %v", code)
|
||||
t.Errorf("expected a 220 greeting, got %v", code)
|
||||
}
|
||||
|
||||
err := playScriptAgainst(t, c, script)
|
||||
playScriptAgainst(t, c, script)
|
||||
|
||||
// Not all tests leave the session in a clean state, so the following two
|
||||
// calls can fail
|
||||
// Not all tests leave the session in a clean state, so the following two calls can fail
|
||||
_, _ = c.Cmd("QUIT")
|
||||
_, _, _ = c.ReadCodeLine(221)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// playScriptAgainst an existing connection, does not handle server greeting
|
||||
func playScriptAgainst(t *testing.T, c *textproto.Conn, script []scriptStep) error {
|
||||
func playScriptAgainst(t *testing.T, c *textproto.Conn, script []scriptStep) {
|
||||
t.Helper()
|
||||
|
||||
for i, step := range script {
|
||||
id, err := c.Cmd(step.send)
|
||||
id, err := c.Cmd("%s", step.send)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Step %d, failed to send %q: %v", i, step.send, err)
|
||||
t.Fatalf("Step %d, failed to send %q: %v", i, step.send, err)
|
||||
}
|
||||
|
||||
c.StartResponse(id)
|
||||
@@ -471,98 +677,10 @@ func playScriptAgainst(t *testing.T, c *textproto.Conn, script []scriptStep) err
|
||||
c.EndResponse(id)
|
||||
|
||||
if err != nil {
|
||||
// Return after c.EndResponse so we don't hang the connection
|
||||
return err
|
||||
// Fail after c.EndResponse so we don't hang the connection
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tests "MAIL FROM" emits BeforeMailAccepted event.
|
||||
func TestBeforeMailAcceptedEventEmitted(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
extHost := extension.NewHost()
|
||||
server := setupSMTPServer(ds, extHost)
|
||||
defer server.Drain()
|
||||
|
||||
var got *event.AddressParts
|
||||
extHost.Events.BeforeMailAccepted.AddListener(
|
||||
"test",
|
||||
func(addr event.AddressParts) *bool {
|
||||
got = &addr
|
||||
return nil
|
||||
})
|
||||
|
||||
// Play and verify SMTP session.
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"QUIT", 221}}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.NotNil(t, got, "BeforeMailListener did not receive Address")
|
||||
assert.Equal(t, "john", got.Local, "Address local part had wrong value")
|
||||
assert.Equal(t, "gmail.com", got.Domain, "Address domain part had wrong value")
|
||||
}
|
||||
|
||||
// Test "MAIL FROM" acts on BeforeMailAccepted event result.
|
||||
func TestBeforeMailAcceptedEventResponse(t *testing.T) {
|
||||
ds := test.NewStore()
|
||||
extHost := extension.NewHost()
|
||||
server := setupSMTPServer(ds, extHost)
|
||||
defer server.Drain()
|
||||
|
||||
var shouldReturn *bool
|
||||
var gotEvent *event.AddressParts
|
||||
extHost.Events.BeforeMailAccepted.AddListener(
|
||||
"test",
|
||||
func(addr event.AddressParts) *bool {
|
||||
gotEvent = &addr
|
||||
return shouldReturn
|
||||
})
|
||||
|
||||
allowRes := true
|
||||
denyRes := false
|
||||
tcs := map[string]struct {
|
||||
script scriptStep // Command to send and SMTP code expected.
|
||||
eventRes *bool // Response to send from event listener.
|
||||
}{
|
||||
"allow": {
|
||||
script: scriptStep{"MAIL FROM:<john@gmail.com>", 250},
|
||||
eventRes: &allowRes,
|
||||
},
|
||||
"deny": {
|
||||
script: scriptStep{"MAIL FROM:<john@gmail.com>", 550},
|
||||
eventRes: &denyRes,
|
||||
},
|
||||
"defer": {
|
||||
script: scriptStep{"MAIL FROM:<john@gmail.com>", 250},
|
||||
eventRes: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tcs {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Reset event listener.
|
||||
shouldReturn = tc.eventRes
|
||||
gotEvent = nil
|
||||
|
||||
// Play and verify SMTP session.
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
tc.script,
|
||||
{"QUIT", 221}}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.NotNil(t, gotEvent, "BeforeMailListener did not receive Address")
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// net.Pipe does not implement deadlines
|
||||
@@ -574,6 +692,7 @@ func (m *mockConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (m *mockConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
// Creates an unstarted smtp.Server.
|
||||
func setupSMTPServer(ds storage.Store, extHost *extension.Host) *Server {
|
||||
cfg := &config.Root{
|
||||
MailboxNaming: config.FullNaming,
|
||||
@@ -589,7 +708,7 @@ func setupSMTPServer(ds storage.Store, extHost *extension.Host) *Server {
|
||||
},
|
||||
}
|
||||
|
||||
// Create a server, don't start it.
|
||||
// Create a server, but don't start it.
|
||||
addrPolicy := &policy.Addressing{Config: cfg}
|
||||
manager := &message.StoreManager{Store: ds, ExtHost: extHost}
|
||||
|
||||
@@ -599,11 +718,18 @@ func setupSMTPServer(ds storage.Store, extHost *extension.Host) *Server {
|
||||
var sessionNum int
|
||||
|
||||
func setupSMTPSession(t *testing.T, server *Server) net.Conn {
|
||||
t.Helper()
|
||||
logger := zerolog.New(zerolog.NewTestWriter(t))
|
||||
serverConn, clientConn := net.Pipe()
|
||||
t.Cleanup(func() {
|
||||
_ = clientConn.Close()
|
||||
|
||||
// Drain is required to prevent a test-logging data race. If a (failing) test run is
|
||||
// hanging, this may be the culprit.
|
||||
server.Drain()
|
||||
})
|
||||
|
||||
// Start the session.
|
||||
server.wg.Add(1)
|
||||
sessionNum++
|
||||
go server.startSession(sessionNum, &mockConn{serverConn}, logger)
|
||||
|
||||
|
||||
@@ -154,8 +154,8 @@ func (s *Server) serve(ctx context.Context) {
|
||||
} else {
|
||||
tempDelay *= 2
|
||||
}
|
||||
if max := 1 * time.Second; tempDelay > max {
|
||||
tempDelay = max
|
||||
if maxDelay := 1 * time.Second; tempDelay > maxDelay {
|
||||
tempDelay = maxDelay
|
||||
}
|
||||
log.Error().Str("module", "smtp").Err(err).
|
||||
Msgf("SMTP accept timeout; retrying in %v", tempDelay)
|
||||
@@ -176,8 +176,6 @@ func (s *Server) serve(ctx context.Context) {
|
||||
}
|
||||
} else {
|
||||
tempDelay = 0
|
||||
expConnectsTotal.Add(1)
|
||||
s.wg.Add(1)
|
||||
go s.startSession(sessionID, conn, log.Logger)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/config"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -86,13 +85,13 @@ func requestLoggingWrapper(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// spaTemplateHandler creates a handler to serve the index.html template for our SPA.
|
||||
func spaTemplateHandler(tmpl *template.Template, basePath string,
|
||||
webConfig config.Web) http.Handler {
|
||||
func spaTemplateHandler(tmpl *template.Template, basePath string) http.Handler {
|
||||
tmplData := struct {
|
||||
BasePath string
|
||||
}{
|
||||
BasePath: basePath,
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
// ensure we do now allow click jacking
|
||||
w.Header().Set("X-Frame-Options", "SameOrigin")
|
||||
|
||||
@@ -21,6 +21,6 @@ func TextToHTML(text string) string {
|
||||
|
||||
// WrapURL wraps a <a href> tag around the provided URL
|
||||
func WrapURL(url string) string {
|
||||
unescaped := strings.Replace(url, "&", "&", -1)
|
||||
unescaped := strings.ReplaceAll(url, "&", "&")
|
||||
return fmt.Sprintf("<a href=\"%s\" target=\"_blank\">%s</a>", unescaped, url)
|
||||
}
|
||||
|
||||
@@ -51,11 +51,7 @@ type Server struct {
|
||||
}
|
||||
|
||||
// NewServer sets up things for unit tests or the Start() method.
|
||||
func NewServer(
|
||||
conf *config.Root,
|
||||
mm message.Manager,
|
||||
mh *msghub.Hub) *Server {
|
||||
|
||||
func NewServer(conf *config.Root, mm message.Manager, mh *msghub.Hub) *Server {
|
||||
rootConfig = conf
|
||||
|
||||
// NewContext() will use this DataStore for the web handlers.
|
||||
@@ -113,7 +109,7 @@ func NewServer(
|
||||
|
||||
// SPA managed paths.
|
||||
spaHandler := cookieHandler(appConfigCookie(conf.Web),
|
||||
spaTemplateHandler(indexTmpl, prefix("/"), conf.Web))
|
||||
spaTemplateHandler(indexTmpl, prefix("/")))
|
||||
Router.Path(prefix("/")).Handler(spaHandler)
|
||||
Router.Path(prefix("/monitor")).Handler(spaHandler)
|
||||
Router.Path(prefix("/status")).Handler(spaHandler)
|
||||
@@ -134,6 +130,11 @@ func NewServer(
|
||||
|
||||
// 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),
|
||||
@@ -144,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")
|
||||
|
||||
@@ -2,6 +2,7 @@ package file
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -55,7 +56,7 @@ type Store struct {
|
||||
func New(cfg config.Storage, extHost *extension.Host) (storage.Store, error) {
|
||||
path := cfg.Params["path"]
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("'path' parameter not specified")
|
||||
return nil, errors.New("'path' parameter not specified")
|
||||
}
|
||||
|
||||
mailPath := getMailPath(path)
|
||||
|
||||
@@ -2,20 +2,15 @@ package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/mail"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/config"
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension"
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
"github.com/inbucket/inbucket/v3/pkg/message"
|
||||
"github.com/inbucket/inbucket/v3/pkg/storage"
|
||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -38,7 +33,7 @@ func TestSuite(t *testing.T) {
|
||||
func TestFSNew(t *testing.T) {
|
||||
// Should fail if no path specified.
|
||||
ds, err := New(config.Storage{}, extension.NewHost())
|
||||
assert.ErrorContains(t, err, "parameter not specified")
|
||||
require.ErrorContains(t, err, "parameter not specified")
|
||||
assert.Nil(t, ds)
|
||||
}
|
||||
|
||||
@@ -73,7 +68,7 @@ func TestFSDirStructure(t *testing.T) {
|
||||
assert.False(t, isDir(expect), "Expected %q to not exist", expect)
|
||||
|
||||
// Deliver test message
|
||||
id1, _ := deliverMessage(ds, mbName, "test", time.Now())
|
||||
id1, _ := test.DeliverToStore(t, ds, mbName, "test", time.Now())
|
||||
|
||||
// Check path to message exists
|
||||
assert.True(t, isDir(expect), "Expected %q to be a directory", expect)
|
||||
@@ -90,7 +85,7 @@ func TestFSDirStructure(t *testing.T) {
|
||||
assert.True(t, isFile(expect), "Expected %q to be a file", expect)
|
||||
|
||||
// Deliver second test message
|
||||
id2, _ := deliverMessage(ds, mbName, "test 2", time.Now())
|
||||
id2, _ := test.DeliverToStore(t, ds, mbName, "test 2", time.Now())
|
||||
|
||||
// Check files
|
||||
expect = filepath.Join(mbPath, "index.gob")
|
||||
@@ -100,7 +95,7 @@ func TestFSDirStructure(t *testing.T) {
|
||||
|
||||
// Delete message
|
||||
err := ds.RemoveMessage(mbName, id1)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Message should be removed
|
||||
expect = filepath.Join(mbPath, id1+".raw")
|
||||
@@ -110,7 +105,7 @@ func TestFSDirStructure(t *testing.T) {
|
||||
|
||||
// Delete message
|
||||
err = ds.RemoveMessage(mbName, id2)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Message should be removed
|
||||
expect = filepath.Join(mbPath, id2+".raw")
|
||||
@@ -141,21 +136,21 @@ func TestFSMissing(t *testing.T) {
|
||||
|
||||
for i, subj := range subjects {
|
||||
// Add a message
|
||||
id, _ := deliverMessage(ds, mbName, subj, time.Now())
|
||||
id, _ := test.DeliverToStore(t, ds, mbName, subj, time.Now())
|
||||
sentIds[i] = id
|
||||
}
|
||||
|
||||
// Delete a message file without removing it from index
|
||||
msg, err := ds.GetMessage(mbName, sentIds[1])
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
fmsg := msg.(*Message)
|
||||
_ = os.Remove(fmsg.rawPath())
|
||||
msg, err = ds.GetMessage(mbName, sentIds[1])
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to read parts of message
|
||||
_, err = msg.Source()
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
|
||||
if t.Failed() {
|
||||
// Wait for handler to finish logging
|
||||
@@ -176,29 +171,29 @@ func TestGetLatestMessage(t *testing.T) {
|
||||
// Test empty mailbox
|
||||
msg, err := ds.GetMessage(mbName, "latest")
|
||||
assert.Nil(t, msg)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
|
||||
// Deliver test message
|
||||
deliverMessage(ds, mbName, "test", time.Now())
|
||||
test.DeliverToStore(t, ds, mbName, "test", time.Now())
|
||||
|
||||
// Deliver test message 2
|
||||
id2, _ := deliverMessage(ds, mbName, "test 2", time.Now())
|
||||
id2, _ := test.DeliverToStore(t, ds, mbName, "test 2", time.Now())
|
||||
|
||||
// Test get the latest message
|
||||
msg, err = ds.GetMessage(mbName, "latest")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, msg.ID() == id2, "Expected %q to be equal to %q", msg.ID(), id2)
|
||||
assert.Equal(t, id2, msg.ID(), "Expected %q to be equal to %q", msg.ID(), id2)
|
||||
|
||||
// Deliver test message 3
|
||||
id3, _ := deliverMessage(ds, mbName, "test 3", time.Now())
|
||||
id3, _ := test.DeliverToStore(t, ds, mbName, "test 3", time.Now())
|
||||
|
||||
msg, err = ds.GetMessage(mbName, "latest")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, msg.ID() == id3, "Expected %q to be equal to %q", msg.ID(), id3)
|
||||
assert.Equal(t, id3, msg.ID(), "Expected %q to be equal to %q", msg.ID(), id3)
|
||||
|
||||
// Test wrong id
|
||||
_, err = ds.GetMessage(mbName, "wrongid")
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
|
||||
if t.Failed() {
|
||||
// Wait for handler to finish logging
|
||||
@@ -231,30 +226,6 @@ func setupDataStore(cfg config.Storage, extHost *extension.Host) (*Store, *bytes
|
||||
return s.(*Store), buf
|
||||
}
|
||||
|
||||
// deliverMessage creates and delivers a message to the specific mailbox, returning
|
||||
// the size of the generated message.
|
||||
func deliverMessage(ds *Store, mbName string, subject string, date time.Time) (string, int64) {
|
||||
// Build message for delivery
|
||||
meta := event.MessageMetadata{
|
||||
Mailbox: mbName,
|
||||
To: []*mail.Address{{Name: "", Address: "somebody@host"}},
|
||||
From: &mail.Address{Name: "", Address: "somebodyelse@host"},
|
||||
Subject: subject,
|
||||
Date: date,
|
||||
}
|
||||
testMsg := fmt.Sprintf("To: %s\r\nFrom: %s\r\nSubject: %s\r\n\r\nTest Body\r\n",
|
||||
meta.To[0].Address, meta.From.Address, subject)
|
||||
delivery := &message.Delivery{
|
||||
Meta: meta,
|
||||
Reader: io.NopCloser(strings.NewReader(testMsg)),
|
||||
}
|
||||
id, err := ds.AddMessage(delivery)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return id, int64(len(testMsg))
|
||||
}
|
||||
|
||||
func teardownDataStore(ds *Store) {
|
||||
if err := os.RemoveAll(ds.path); err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -112,6 +112,8 @@ func (mb *mbox) readIndex() error {
|
||||
log.Debug().Str("module", "storage").Str("path", mb.indexPath).
|
||||
Msg("Index does not yet exist")
|
||||
mb.indexLoaded = true
|
||||
|
||||
//lint:ignore nilerr missing mailboxes are considered empty.
|
||||
return nil
|
||||
}
|
||||
file, err := os.Open(mb.indexPath)
|
||||
@@ -130,7 +132,7 @@ func (mb *mbox) readIndex() error {
|
||||
dec := gob.NewDecoder(br)
|
||||
name := ""
|
||||
if err = dec.Decode(&name); err != nil {
|
||||
return fmt.Errorf("Corrupt mailbox %q: %v", mb.indexPath, err)
|
||||
return fmt.Errorf("corrupt mailbox %q: %v", mb.indexPath, err)
|
||||
}
|
||||
mb.name = name
|
||||
for {
|
||||
@@ -140,7 +142,7 @@ func (mb *mbox) readIndex() error {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("Corrupt mailbox %q: %v", mb.indexPath, err)
|
||||
return fmt.Errorf("corrupt mailbox %q: %v", mb.indexPath, err)
|
||||
}
|
||||
msg.mailbox = mb
|
||||
mb.messages = append(mb.messages, msg)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
// access in most cases without requiring an infinite number of mutexes.
|
||||
type HashLock [4096]sync.RWMutex
|
||||
|
||||
// Get returns a RWMutex based on the first 12 bits of the mailbox hash. Hash must be a hexidecimal
|
||||
// Get returns a RWMutex based on the first 12 bits of the mailbox hash. Hash must be a hexadecimal
|
||||
// string of three or more characters.
|
||||
func (h *HashLock) Get(hash string) *sync.RWMutex {
|
||||
if len(hash) < 3 {
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestHashLock(t *testing.T) {
|
||||
t.Run(ts, func(t *testing.T) {
|
||||
l := hl.Get(ts)
|
||||
if l == nil {
|
||||
t.Errorf("Expeced non-nil lock for hex string %q", ts)
|
||||
t.Errorf("Expected non-nil lock for hex string %q", ts)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ type mbox struct {
|
||||
|
||||
var _ storage.Store = &Store{}
|
||||
|
||||
// New returns an emtpy memory store.
|
||||
// New returns an empty memory store.
|
||||
func New(cfg config.Storage, extHost *extension.Host) (storage.Store, error) {
|
||||
s := &Store{
|
||||
boxes: make(map[string]*mbox),
|
||||
@@ -82,6 +82,7 @@ func (s *Store) AddMessage(message storage.Message) (id string, err error) {
|
||||
m.id = id
|
||||
m.source = source
|
||||
mb.messages[id] = m
|
||||
|
||||
if s.cap > 0 {
|
||||
// Enforce cap.
|
||||
for len(mb.messages) > s.cap {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package stringutil
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net/mail"
|
||||
"strings"
|
||||
@@ -13,10 +13,11 @@ import (
|
||||
func HashMailboxName(mailbox string) string {
|
||||
h := sha1.New()
|
||||
if _, err := io.WriteString(h, mailbox); err != nil {
|
||||
// This shouldn't ever happen
|
||||
// This should never happen.
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// StringAddress converts an Address to a UTF-8 string.
|
||||
@@ -95,11 +96,9 @@ func MatchWithWildcards(p string, s string) bool {
|
||||
if runePattern[j-1] == '*' {
|
||||
isMatchingMatrix[0][j] = isMatchingMatrix[0][j-1]
|
||||
}
|
||||
|
||||
}
|
||||
for i := 1; i <= lenInput; i++ {
|
||||
for j := 1; j <= lenPattern; j++ {
|
||||
|
||||
if runePattern[j-1] == '*' {
|
||||
isMatchingMatrix[i][j] = isMatchingMatrix[i-1][j] || isMatchingMatrix[i][j-1]
|
||||
}
|
||||
|
||||
@@ -6,14 +6,17 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/stringutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHashMailboxName(t *testing.T) {
|
||||
want := "1d6e1cf70ec6f9ab28d3ea4b27a49a77654d370e"
|
||||
got := stringutil.HashMailboxName("mail")
|
||||
if got != want {
|
||||
t.Errorf("Got %q, want %q", got, want)
|
||||
}
|
||||
want := "da39a3ee5e6b4b0d3255bfef95601890afd80709"
|
||||
got := stringutil.HashMailboxName("")
|
||||
assert.Equal(t, want, got, "for empty string")
|
||||
|
||||
want = "1d6e1cf70ec6f9ab28d3ea4b27a49a77654d370e"
|
||||
got = stringutil.HashMailboxName("mail")
|
||||
assert.Equal(t, want, got, "for 'mail'")
|
||||
}
|
||||
|
||||
func TestStringAddressList(t *testing.T) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/jhillyerd/goldiff"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,172 +36,128 @@ const (
|
||||
)
|
||||
|
||||
// TODO: Add suites for domain and full addressing modes.
|
||||
|
||||
func TestSuite(t *testing.T) {
|
||||
stopServer, err := startServer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stopServer()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
test func(*testing.T)
|
||||
}{
|
||||
{"basic", testBasic},
|
||||
{"fullname", testFullname},
|
||||
{"encodedHeader", testEncodedHeader},
|
||||
{"ipv4Recipient", testIPv4Recipient},
|
||||
{"ipv6Recipient", testIPv6Recipient},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, tc.test)
|
||||
}
|
||||
type IntegrationSuite struct {
|
||||
suite.Suite
|
||||
stopServer func()
|
||||
}
|
||||
|
||||
func testBasic(t *testing.T) {
|
||||
func (s *IntegrationSuite) SetupSuite() {
|
||||
stopServer, err := startServer()
|
||||
s.Require().NoError(err)
|
||||
s.stopServer = stopServer
|
||||
}
|
||||
|
||||
func (s *IntegrationSuite) TearDownSuite() {
|
||||
s.stopServer()
|
||||
}
|
||||
|
||||
func TestIntegrationSuite(t *testing.T) {
|
||||
suite.Run(t, new(IntegrationSuite))
|
||||
}
|
||||
|
||||
func (s *IntegrationSuite) TestBasic() {
|
||||
client, err := client.New(restBaseURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
from := "fromuser@inbucket.org"
|
||||
to := []string{"recipient@inbucket.org"}
|
||||
input := readTestData("basic.txt")
|
||||
|
||||
// Send mail.
|
||||
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Confirm receipt.
|
||||
msg, err := client.GetMessage("recipient", "latest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if msg == nil {
|
||||
t.Errorf("Got nil message, wanted non-nil message.")
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
s.NotNil(msg)
|
||||
|
||||
// Compare to golden.
|
||||
got := formatMessage(msg)
|
||||
goldiff.File(t, got, "testdata", "basic.golden")
|
||||
goldiff.File(s.T(), got, "testdata", "basic.golden")
|
||||
}
|
||||
|
||||
func testFullname(t *testing.T) {
|
||||
func (s *IntegrationSuite) TestFullname() {
|
||||
client, err := client.New(restBaseURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
from := "fromuser@inbucket.org"
|
||||
to := []string{"recipient@inbucket.org"}
|
||||
input := readTestData("fullname.txt")
|
||||
|
||||
// Send mail.
|
||||
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Confirm receipt.
|
||||
msg, err := client.GetMessage("recipient", "latest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if msg == nil {
|
||||
t.Errorf("Got nil message, wanted non-nil message.")
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
s.NotNil(msg)
|
||||
|
||||
// Compare to golden.
|
||||
got := formatMessage(msg)
|
||||
goldiff.File(t, got, "testdata", "fullname.golden")
|
||||
goldiff.File(s.T(), got, "testdata", "fullname.golden")
|
||||
}
|
||||
|
||||
func testEncodedHeader(t *testing.T) {
|
||||
func (s *IntegrationSuite) TestEncodedHeader() {
|
||||
client, err := client.New(restBaseURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
from := "fromuser@inbucket.org"
|
||||
to := []string{"recipient@inbucket.org"}
|
||||
input := readTestData("encodedheader.txt")
|
||||
|
||||
// Send mail.
|
||||
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Confirm receipt.
|
||||
msg, err := client.GetMessage("recipient", "latest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if msg == nil {
|
||||
t.Errorf("Got nil message, wanted non-nil message.")
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
s.NotNil(msg)
|
||||
|
||||
// Compare to golden.
|
||||
got := formatMessage(msg)
|
||||
goldiff.File(t, got, "testdata", "encodedheader.golden")
|
||||
goldiff.File(s.T(), got, "testdata", "encodedheader.golden")
|
||||
}
|
||||
|
||||
func testIPv4Recipient(t *testing.T) {
|
||||
func (s *IntegrationSuite) TestIPv4Recipient() {
|
||||
client, err := client.New(restBaseURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
from := "fromuser@inbucket.org"
|
||||
to := []string{"ip4recipient@[192.168.123.123]"}
|
||||
input := readTestData("no-to.txt")
|
||||
|
||||
// Send mail.
|
||||
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Confirm receipt.
|
||||
msg, err := client.GetMessage("ip4recipient", "latest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if msg == nil {
|
||||
t.Errorf("Got nil message, wanted non-nil message.")
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
s.NotNil(msg)
|
||||
|
||||
// Compare to golden.
|
||||
got := formatMessage(msg)
|
||||
goldiff.File(t, got, "testdata", "no-to-ipv4.golden")
|
||||
goldiff.File(s.T(), got, "testdata", "no-to-ipv4.golden")
|
||||
}
|
||||
|
||||
func testIPv6Recipient(t *testing.T) {
|
||||
func (s *IntegrationSuite) TestIPv6Recipient() {
|
||||
client, err := client.New(restBaseURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
from := "fromuser@inbucket.org"
|
||||
to := []string{"ip6recipient@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]"}
|
||||
input := readTestData("no-to.txt")
|
||||
|
||||
// Send mail.
|
||||
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Confirm receipt.
|
||||
msg, err := client.GetMessage("ip6recipient", "latest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if msg == nil {
|
||||
t.Errorf("Got nil message, wanted non-nil message.")
|
||||
}
|
||||
s.Require().NoError(err)
|
||||
s.NotNil(msg)
|
||||
|
||||
// Compare to golden.
|
||||
got := formatMessage(msg)
|
||||
goldiff.File(t, got, "testdata", "no-to-ipv6.golden")
|
||||
goldiff.File(s.T(), got, "testdata", "no-to-ipv6.golden")
|
||||
}
|
||||
|
||||
func formatMessage(m *client.Message) []byte {
|
||||
@@ -282,8 +239,7 @@ func clearEnv() {
|
||||
}
|
||||
|
||||
// Backup ciritcal env variables.
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if runtime.GOOS == "windows" {
|
||||
backup("SYSTEMROOT")
|
||||
}
|
||||
|
||||
|
||||
90
pkg/test/lua.go
Normal file
90
pkg/test/lua.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cosmotek/loguago"
|
||||
"github.com/rs/zerolog"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// LuaInit holds useful test globals.
|
||||
const LuaInit = `
|
||||
local logger = require("logger")
|
||||
|
||||
async = false
|
||||
asserts_ok = true
|
||||
|
||||
-- With async: marks tests as failed via asserts_ok, logs error.
|
||||
-- Without async: erroring when tests fail.
|
||||
function assert_async(value, message, label)
|
||||
if not value then
|
||||
if label then
|
||||
message = string.format("%s for %s", message, label)
|
||||
end
|
||||
|
||||
if async then
|
||||
logger.error(message, {from = "assert_async"})
|
||||
asserts_ok = false
|
||||
else
|
||||
error(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Verifies plain values and list-style tables.
|
||||
function assert_eq(got, want, label)
|
||||
if type(got) == "table" and type(want) == "table" then
|
||||
assert_async(#got == #want,
|
||||
string.format("got %d elements, wanted %d", #got, #want), label)
|
||||
|
||||
for i, gotv in ipairs(got) do
|
||||
local wantv = want[i]
|
||||
assert_eq(gotv, wantv,
|
||||
string.format("got[%d] = %q, wanted %q", gotv, wantv), label)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
assert_async(got == want, string.format("got %q, wanted %q", got, want), label)
|
||||
end
|
||||
|
||||
-- Verifies string want contains string got.
|
||||
function assert_contains(got, want, label)
|
||||
assert_async(string.find(got, want),
|
||||
string.format("got %q, wanted it to contain %q", got, want), label)
|
||||
end
|
||||
`
|
||||
|
||||
// NewLuaState creates a new Lua LState initialized with logging and the test helpers in `LuaInit`.
|
||||
//
|
||||
// Returns a pointer to the created LState and a string builder to hold the log output.
|
||||
func NewLuaState() (*lua.LState, *strings.Builder) {
|
||||
output := &strings.Builder{}
|
||||
logger := loguago.NewLogger(zerolog.New(output))
|
||||
|
||||
ls := lua.NewState()
|
||||
ls.PreloadModule("logger", logger.Loader)
|
||||
if err := ls.DoString(LuaInit); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return ls, output
|
||||
}
|
||||
|
||||
// AssertNotified requires a truthy LValue on the notify channel.
|
||||
func AssertNotified(t *testing.T, notify chan lua.LValue) {
|
||||
t.Helper()
|
||||
select {
|
||||
case reslv := <-notify:
|
||||
// Lua function received event.
|
||||
if lua.LVIsFalse(reslv) {
|
||||
t.Error("Lua responsed with false, wanted true")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("Lua did not respond to event within timeout")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
61
pkg/test/storage_helpers.go
Normal file
61
pkg/test/storage_helpers.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
"github.com/inbucket/inbucket/v3/pkg/message"
|
||||
"github.com/inbucket/inbucket/v3/pkg/storage"
|
||||
)
|
||||
|
||||
// DeliverToStore creates and delivers a message to the specific mailbox, returning the size of the
|
||||
// generated message.
|
||||
func DeliverToStore(
|
||||
t *testing.T,
|
||||
store storage.Store,
|
||||
mailbox string,
|
||||
subject string,
|
||||
date time.Time,
|
||||
) (string, int64) {
|
||||
t.Helper()
|
||||
meta := event.MessageMetadata{
|
||||
Mailbox: mailbox,
|
||||
To: []*mail.Address{{Name: "Some Body", Address: "somebody@host"}},
|
||||
From: &mail.Address{Name: "Some B. Else", Address: "somebodyelse@host"},
|
||||
Subject: subject,
|
||||
Date: date,
|
||||
}
|
||||
testMsg := fmt.Sprintf("To: %s\r\nFrom: %s\r\nSubject: %s\r\n\r\nTest Body\r\n",
|
||||
meta.To[0].Address, meta.From.Address, subject)
|
||||
delivery := &message.Delivery{
|
||||
Meta: meta,
|
||||
Reader: io.NopCloser(strings.NewReader(testMsg)),
|
||||
}
|
||||
|
||||
id, err := store.AddMessage(delivery)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return id, int64(len(testMsg))
|
||||
}
|
||||
|
||||
// GetAndCountMessages is a test helper that expects to receive count messages or fails the test, it
|
||||
// also checks return error.
|
||||
func GetAndCountMessages(t *testing.T, s storage.Store, mailbox string, count int) []storage.Message {
|
||||
t.Helper()
|
||||
msgs, err := s.GetMessages(mailbox)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to GetMessages for %q: %v", mailbox, err)
|
||||
}
|
||||
if len(msgs) != count {
|
||||
t.Errorf("Got %v messages for %q, want: %v", len(msgs), mailbox, count)
|
||||
}
|
||||
|
||||
return msgs
|
||||
}
|
||||
@@ -22,11 +22,19 @@ import (
|
||||
type StoreFactory func(
|
||||
config.Storage, *extension.Host) (store storage.Store, destroy func(), err error)
|
||||
|
||||
// storeSuite is passed to each test function; embeds `testing.T` to provide testing primitives.
|
||||
type storeSuite struct {
|
||||
*testing.T
|
||||
store storage.Store
|
||||
extHost *extension.Host
|
||||
}
|
||||
|
||||
// StoreSuite runs a set of general tests on the provided Store.
|
||||
func StoreSuite(t *testing.T, factory StoreFactory) {
|
||||
t.Helper()
|
||||
testCases := []struct {
|
||||
name string
|
||||
test func(*testing.T, storage.Store, *extension.Host)
|
||||
test func(storeSuite)
|
||||
conf config.Storage
|
||||
}{
|
||||
{"metadata", testMetadata, config.Storage{}},
|
||||
@@ -49,14 +57,20 @@ func StoreSuite(t *testing.T, factory StoreFactory) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tc.test(t, store, extHost)
|
||||
destroy()
|
||||
defer destroy()
|
||||
|
||||
s := storeSuite{
|
||||
T: t,
|
||||
store: store,
|
||||
extHost: extHost,
|
||||
}
|
||||
tc.test(s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testMetadata verifies message metadata is stored and retrieved correctly.
|
||||
func testMetadata(t *testing.T, store storage.Store, extHost *extension.Host) {
|
||||
func testMetadata(s storeSuite) {
|
||||
mailbox := "testmailbox"
|
||||
from := &mail.Address{Name: "From Person", Address: "from@person.com"}
|
||||
to := []*mail.Address{
|
||||
@@ -78,54 +92,54 @@ func testMetadata(t *testing.T, store storage.Store, extHost *extension.Host) {
|
||||
},
|
||||
Reader: strings.NewReader(content),
|
||||
}
|
||||
id, err := store.AddMessage(delivery)
|
||||
id, err := s.store.AddMessage(delivery)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
s.Fatal(err)
|
||||
}
|
||||
if id == "" {
|
||||
t.Fatal("Expected AddMessage() to return non-empty ID string")
|
||||
s.Fatal("Expected AddMessage() to return non-empty ID string")
|
||||
}
|
||||
// Retrieve and validate the message.
|
||||
sm, err := store.GetMessage(mailbox, id)
|
||||
sm, err := s.store.GetMessage(mailbox, id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
s.Fatal(err)
|
||||
}
|
||||
if sm.Mailbox() != mailbox {
|
||||
t.Errorf("got mailbox %q, want: %q", sm.Mailbox(), mailbox)
|
||||
s.Errorf("got mailbox %q, want: %q", sm.Mailbox(), mailbox)
|
||||
}
|
||||
if sm.ID() != id {
|
||||
t.Errorf("got id %q, want: %q", sm.ID(), id)
|
||||
s.Errorf("got id %q, want: %q", sm.ID(), id)
|
||||
}
|
||||
if *sm.From() != *from {
|
||||
t.Errorf("got from %v, want: %v", sm.From(), from)
|
||||
s.Errorf("got from %v, want: %v", sm.From(), from)
|
||||
}
|
||||
if len(sm.To()) != len(to) {
|
||||
t.Errorf("got len(to) = %v, want: %v", len(sm.To()), len(to))
|
||||
s.Errorf("got len(to) = %v, want: %v", len(sm.To()), len(to))
|
||||
} else {
|
||||
for i, got := range sm.To() {
|
||||
if *to[i] != *got {
|
||||
t.Errorf("got to[%v] %v, want: %v", i, got, to[i])
|
||||
s.Errorf("got to[%v] %v, want: %v", i, got, to[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
if !sm.Date().Equal(date) {
|
||||
t.Errorf("got date %v, want: %v", sm.Date(), date)
|
||||
s.Errorf("got date %v, want: %v", sm.Date(), date)
|
||||
}
|
||||
if sm.Subject() != subject {
|
||||
t.Errorf("got subject %q, want: %q", sm.Subject(), subject)
|
||||
s.Errorf("got subject %q, want: %q", sm.Subject(), subject)
|
||||
}
|
||||
if sm.Size() != int64(len(content)) {
|
||||
t.Errorf("got size %v, want: %v", sm.Size(), len(content))
|
||||
s.Errorf("got size %v, want: %v", sm.Size(), len(content))
|
||||
}
|
||||
if sm.Seen() {
|
||||
t.Errorf("got seen %v, want: false", sm.Seen())
|
||||
s.Errorf("got seen %v, want: false", sm.Seen())
|
||||
}
|
||||
}
|
||||
|
||||
// testContent generates some binary content and makes sure it is correctly retrieved.
|
||||
func testContent(t *testing.T, store storage.Store, extHost *extension.Host) {
|
||||
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"
|
||||
@@ -146,332 +160,287 @@ func testContent(t *testing.T, store storage.Store, extHost *extension.Host) {
|
||||
},
|
||||
Reader: bytes.NewReader(content),
|
||||
}
|
||||
id, err := store.AddMessage(delivery)
|
||||
require.NoError(t, err, "AddMessage() failed")
|
||||
id, err := s.store.AddMessage(delivery)
|
||||
require.NoError(s, err, "AddMessage() failed")
|
||||
|
||||
// Read stored message source.
|
||||
m, err := store.GetMessage(mailbox, id)
|
||||
require.NoError(t, err, "GetMessage() failed")
|
||||
m, err := s.store.GetMessage(mailbox, id)
|
||||
require.NoError(s, err, "GetMessage() failed")
|
||||
r, err := m.Source()
|
||||
require.NoError(t, err, "Source() failed")
|
||||
require.NoError(s, err, "Source() failed")
|
||||
got, err := io.ReadAll(r)
|
||||
require.NoError(t, err, "failed to read source")
|
||||
require.NoError(s, err, "failed to read source")
|
||||
err = r.Close()
|
||||
assert.NoError(t, err, "failed to close source reader")
|
||||
require.NoError(s, err, "failed to close source reader")
|
||||
|
||||
// Verify source.
|
||||
if len(got) != len(content) {
|
||||
t.Errorf("Got len(content) == %v, want: %v", len(got), len(content))
|
||||
s.Errorf("Got len(content) == %v, want: %v", len(got), len(content))
|
||||
}
|
||||
errors := 0
|
||||
for i, b := range got {
|
||||
if b != content[i] {
|
||||
t.Errorf("Got content[%v] == %v, want: %v", i, b, content[i])
|
||||
s.Errorf("Got content[%v] == %v, want: %v", i, b, content[i])
|
||||
errors++
|
||||
}
|
||||
if errors > 5 {
|
||||
t.Fatalf("Too many content errors, aborting test.")
|
||||
s.Fatalf("Too many content errors, aborting test.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testDeliveryOrder delivers several messages to the same mailbox, meanwhile querying its contents
|
||||
// with a new GetMessages call each cycle.
|
||||
func testDeliveryOrder(t *testing.T, store storage.Store, extHost *extension.Host) {
|
||||
func testDeliveryOrder(s storeSuite) {
|
||||
mailbox := "fred"
|
||||
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
||||
for i, subj := range subjects {
|
||||
// Check mailbox count.
|
||||
GetAndCountMessages(t, store, mailbox, i)
|
||||
DeliverToStore(t, store, mailbox, subj, time.Now())
|
||||
GetAndCountMessages(s.T, s.store, mailbox, i)
|
||||
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
||||
}
|
||||
// Confirm delivery order.
|
||||
msgs := GetAndCountMessages(t, store, mailbox, 5)
|
||||
msgs := GetAndCountMessages(s.T, s.store, mailbox, 5)
|
||||
for i, want := range subjects {
|
||||
got := msgs[i].Subject()
|
||||
if got != want {
|
||||
t.Errorf("Got subject %q, want %q", got, want)
|
||||
s.Errorf("Got subject %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testLatest delivers several messages to the same mailbox, and confirms the id `latest` returns
|
||||
// the last message sent.
|
||||
func testLatest(t *testing.T, store storage.Store, extHost *extension.Host) {
|
||||
func testLatest(s storeSuite) {
|
||||
mailbox := "fred"
|
||||
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
||||
for _, subj := range subjects {
|
||||
DeliverToStore(t, store, mailbox, subj, time.Now())
|
||||
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
||||
}
|
||||
// Confirm latest.
|
||||
latest, err := store.GetMessage(mailbox, "latest")
|
||||
latest, err := s.store.GetMessage(mailbox, "latest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
s.Fatal(err)
|
||||
}
|
||||
if latest == nil {
|
||||
t.Fatalf("Got nil message, wanted most recent message for %v.", mailbox)
|
||||
s.Fatalf("Got nil message, wanted most recent message for %v.", mailbox)
|
||||
}
|
||||
got := latest.Subject()
|
||||
want := "echo"
|
||||
if got != want {
|
||||
t.Errorf("Got subject %q, want %q", got, want)
|
||||
s.Errorf("Got subject %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// testNaming ensures the store does not enforce local part mailbox naming.
|
||||
func testNaming(t *testing.T, store storage.Store, extHost *extension.Host) {
|
||||
DeliverToStore(t, store, "fred@fish.net", "disk #27", time.Now())
|
||||
GetAndCountMessages(t, store, "fred", 0)
|
||||
GetAndCountMessages(t, store, "fred@fish.net", 1)
|
||||
func testNaming(s storeSuite) {
|
||||
DeliverToStore(s.T, s.store, "fred@fish.net", "disk #27", time.Now())
|
||||
GetAndCountMessages(s.T, s.store, "fred", 0)
|
||||
GetAndCountMessages(s.T, s.store, "fred@fish.net", 1)
|
||||
}
|
||||
|
||||
// testSize verifies message content size metadata values.
|
||||
func testSize(t *testing.T, store storage.Store, extHost *extension.Host) {
|
||||
func testSize(s storeSuite) {
|
||||
mailbox := "fred"
|
||||
subjects := []string{"a", "br", "much longer than the others"}
|
||||
sentIds := make([]string, len(subjects))
|
||||
sentSizes := make([]int64, len(subjects))
|
||||
for i, subj := range subjects {
|
||||
id, size := DeliverToStore(t, store, mailbox, subj, time.Now())
|
||||
id, size := DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
||||
sentIds[i] = id
|
||||
sentSizes[i] = size
|
||||
}
|
||||
for i, id := range sentIds {
|
||||
msg, err := store.GetMessage(mailbox, id)
|
||||
msg, err := s.store.GetMessage(mailbox, id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
s.Fatal(err)
|
||||
}
|
||||
want := sentSizes[i]
|
||||
got := msg.Size()
|
||||
if got != want {
|
||||
t.Errorf("Got size %v, want: %v", got, want)
|
||||
s.Errorf("Got size %v, want: %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testSeen verifies a message can be marked as seen.
|
||||
func testSeen(t *testing.T, store storage.Store, extHost *extension.Host) {
|
||||
func testSeen(s storeSuite) {
|
||||
mailbox := "lisa"
|
||||
id1, _ := DeliverToStore(t, store, mailbox, "whatever", time.Now())
|
||||
id2, _ := DeliverToStore(t, store, mailbox, "hello?", time.Now())
|
||||
id1, _ := DeliverToStore(s.T, s.store, mailbox, "whatever", time.Now())
|
||||
id2, _ := DeliverToStore(s.T, s.store, mailbox, "hello?", time.Now())
|
||||
// Confirm unseen.
|
||||
msg, err := store.GetMessage(mailbox, id1)
|
||||
msg, err := s.store.GetMessage(mailbox, id1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
s.Fatal(err)
|
||||
}
|
||||
if msg.Seen() {
|
||||
t.Errorf("got seen %v, want: false", msg.Seen())
|
||||
s.Errorf("got seen %v, want: false", msg.Seen())
|
||||
}
|
||||
// Mark id1 seen.
|
||||
err = store.MarkSeen(mailbox, id1)
|
||||
err = s.store.MarkSeen(mailbox, id1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
s.Fatal(err)
|
||||
}
|
||||
// Verify id1 seen.
|
||||
msg, err = store.GetMessage(mailbox, id1)
|
||||
msg, err = s.store.GetMessage(mailbox, id1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
s.Fatal(err)
|
||||
}
|
||||
if !msg.Seen() {
|
||||
t.Errorf("id1 got seen %v, want: true", msg.Seen())
|
||||
s.Errorf("id1 got seen %v, want: true", msg.Seen())
|
||||
}
|
||||
// Verify id2 still unseen.
|
||||
msg, err = store.GetMessage(mailbox, id2)
|
||||
msg, err = s.store.GetMessage(mailbox, id2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
s.Fatal(err)
|
||||
}
|
||||
if msg.Seen() {
|
||||
t.Errorf("id2 got seen %v, want: false", msg.Seen())
|
||||
s.Errorf("id2 got seen %v, want: false", msg.Seen())
|
||||
}
|
||||
}
|
||||
|
||||
// testDelete creates and deletes some messages.
|
||||
func testDelete(t *testing.T, store storage.Store, extHost *extension.Host) {
|
||||
func testDelete(s storeSuite) {
|
||||
mailbox := "fred"
|
||||
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
||||
for _, subj := range subjects {
|
||||
DeliverToStore(t, store, mailbox, subj, time.Now())
|
||||
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
||||
}
|
||||
msgs := GetAndCountMessages(t, store, mailbox, len(subjects))
|
||||
msgs := GetAndCountMessages(s.T, s.store, mailbox, len(subjects))
|
||||
|
||||
// Subscribe to events.
|
||||
eventListener := extHost.Events.AfterMessageDeleted.AsyncTestListener("test", 2)
|
||||
eventListener := s.extHost.Events.AfterMessageDeleted.AsyncTestListener("test", 2)
|
||||
|
||||
// Delete a couple messages.
|
||||
deleteIDs := []string{msgs[1].ID(), msgs[3].ID()}
|
||||
for _, id := range deleteIDs {
|
||||
err := store.RemoveMessage(mailbox, id)
|
||||
require.NoError(t, err)
|
||||
err := s.store.RemoveMessage(mailbox, id)
|
||||
require.NoError(s, err)
|
||||
}
|
||||
|
||||
// Confirm deletion.
|
||||
subjects = []string{"alpha", "charlie", "echo"}
|
||||
msgs = GetAndCountMessages(t, store, mailbox, len(subjects))
|
||||
msgs = GetAndCountMessages(s.T, s.store, mailbox, len(subjects))
|
||||
for i, want := range subjects {
|
||||
got := msgs[i].Subject()
|
||||
if got != want {
|
||||
t.Errorf("Got subject %q, want %q", got, want)
|
||||
s.Errorf("Got subject %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Capture events and check correct IDs were emitted.
|
||||
ev1, err := eventListener()
|
||||
require.NoError(t, err)
|
||||
require.NoError(s, err)
|
||||
ev2, err := eventListener()
|
||||
require.NoError(t, err)
|
||||
require.NoError(s, err)
|
||||
eventIDs := []string{ev1.ID, ev2.ID}
|
||||
for _, id := range deleteIDs {
|
||||
assert.Contains(t, eventIDs, id)
|
||||
assert.Contains(s, eventIDs, id)
|
||||
}
|
||||
|
||||
// Try appending one more.
|
||||
DeliverToStore(t, store, mailbox, "foxtrot", time.Now())
|
||||
DeliverToStore(s.T, s.store, mailbox, "foxtrot", time.Now())
|
||||
subjects = []string{"alpha", "charlie", "echo", "foxtrot"}
|
||||
msgs = GetAndCountMessages(t, store, mailbox, len(subjects))
|
||||
msgs = GetAndCountMessages(s.T, s.store, mailbox, len(subjects))
|
||||
for i, want := range subjects {
|
||||
got := msgs[i].Subject()
|
||||
if got != want {
|
||||
t.Errorf("Got subject %q, want %q", got, want)
|
||||
s.Errorf("Got subject %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testPurge makes sure mailboxes can be purged.
|
||||
func testPurge(t *testing.T, store storage.Store, extHost *extension.Host) {
|
||||
func testPurge(s storeSuite) {
|
||||
mailbox := "fred"
|
||||
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
||||
|
||||
// Subscribe to events.
|
||||
eventListener := extHost.Events.AfterMessageDeleted.AsyncTestListener("test", len(subjects))
|
||||
eventListener := s.extHost.Events.AfterMessageDeleted.AsyncTestListener("test", len(subjects))
|
||||
|
||||
// Populate mailbox.
|
||||
for _, subj := range subjects {
|
||||
DeliverToStore(t, store, mailbox, subj, time.Now())
|
||||
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
||||
}
|
||||
GetAndCountMessages(t, store, mailbox, len(subjects))
|
||||
GetAndCountMessages(s.T, s.store, mailbox, len(subjects))
|
||||
|
||||
// Purge and verify.
|
||||
err := store.PurgeMessages(mailbox)
|
||||
require.NoError(t, err)
|
||||
GetAndCountMessages(t, store, mailbox, 0)
|
||||
err := s.store.PurgeMessages(mailbox)
|
||||
require.NoError(s, err)
|
||||
GetAndCountMessages(s.T, s.store, mailbox, 0)
|
||||
|
||||
// Confirm events emitted.
|
||||
gotEvents := []*event.MessageMetadata{}
|
||||
for range subjects {
|
||||
ev, err := eventListener()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
s.Error(err)
|
||||
break
|
||||
}
|
||||
gotEvents = append(gotEvents, ev)
|
||||
}
|
||||
assert.Equal(t, len(subjects), len(gotEvents),
|
||||
assert.Len(s, gotEvents, len(subjects),
|
||||
"expected delete event for each message in mailbox")
|
||||
}
|
||||
|
||||
// testMsgCap verifies the message cap is enforced.
|
||||
func testMsgCap(t *testing.T, store storage.Store, extHost *extension.Host) {
|
||||
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(t, store, mailbox, subj, time.Now())
|
||||
msgs, err := store.GetMessages(mailbox)
|
||||
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
||||
msgs, err := s.store.GetMessages(mailbox)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to GetMessages for %q: %v", mailbox, err)
|
||||
s.Fatalf("Failed to GetMessages for %q: %v", mailbox, err)
|
||||
}
|
||||
if len(msgs) > mbCap {
|
||||
t.Errorf("Mailbox has %v messages, should be capped at %v", len(msgs), mbCap)
|
||||
s.Errorf("Mailbox has %v messages, should be capped at %v", len(msgs), mbCap)
|
||||
break
|
||||
}
|
||||
// Check that the first message is correct.
|
||||
first := i - mbCap + 1
|
||||
if first < 0 {
|
||||
first = 0
|
||||
}
|
||||
|
||||
// Check that the first (oldest) message is correct.
|
||||
first := max(i-mbCap+1, 0)
|
||||
firstSubj := fmt.Sprintf("subject %v", first)
|
||||
if firstSubj != msgs[0].Subject() {
|
||||
t.Errorf("Got subject %q, wanted first subject: %q", msgs[0].Subject(), firstSubj)
|
||||
s.Errorf("Got subject %q, wanted first subject: %q", msgs[0].Subject(), firstSubj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testNoMsgCap verfies a cap of 0 is not enforced.
|
||||
func testNoMsgCap(t *testing.T, store storage.Store, extHost *extension.Host) {
|
||||
func testNoMsgCap(s storeSuite) {
|
||||
mailbox := "captain"
|
||||
for i := 0; i < 20; i++ {
|
||||
for i := range 20 {
|
||||
subj := fmt.Sprintf("subject %v", i)
|
||||
DeliverToStore(t, store, mailbox, subj, time.Now())
|
||||
GetAndCountMessages(t, store, mailbox, i+1)
|
||||
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
||||
GetAndCountMessages(s.T, s.store, mailbox, i+1)
|
||||
}
|
||||
}
|
||||
|
||||
// testVisitMailboxes creates some mailboxes and confirms the VisitMailboxes method visits all of
|
||||
// them.
|
||||
func testVisitMailboxes(t *testing.T, ds storage.Store, extHost *extension.Host) {
|
||||
func testVisitMailboxes(s storeSuite) {
|
||||
// Deliver 2 test messages to each of 5 mailboxes.
|
||||
boxes := []string{"abby", "bill", "christa", "donald", "evelyn"}
|
||||
for _, name := range boxes {
|
||||
DeliverToStore(t, ds, name, "Old Message", time.Now().Add(-24*time.Hour))
|
||||
DeliverToStore(t, ds, name, "New Message", time.Now())
|
||||
DeliverToStore(s.T, s.store, name, "Old Message", time.Now().Add(-24*time.Hour))
|
||||
DeliverToStore(s.T, s.store, name, "New Message", time.Now())
|
||||
}
|
||||
|
||||
// Verify message and mailbox counts.
|
||||
nboxes := 0
|
||||
err := ds.VisitMailboxes(func(messages []storage.Message) bool {
|
||||
err := s.store.VisitMailboxes(func(messages []storage.Message) bool {
|
||||
nboxes++
|
||||
name := "unknown"
|
||||
if len(messages) > 0 {
|
||||
name = messages[0].Mailbox()
|
||||
}
|
||||
|
||||
assert.Equal(t, 2, len(messages), "incorrect message count in mailbox %s", name)
|
||||
assert.Len(s, messages, 2, "incorrect message count in mailbox %s", name)
|
||||
return true
|
||||
})
|
||||
assert.NoError(t, err, "VisitMailboxes() failed")
|
||||
assert.Equal(t, 5, nboxes, "visited %v mailboxes, want: 5", nboxes)
|
||||
}
|
||||
|
||||
// DeliverToStore creates and delivers a message to the specific mailbox, returning the size of the
|
||||
// generated message.
|
||||
func DeliverToStore(
|
||||
t *testing.T,
|
||||
store storage.Store,
|
||||
mailbox string,
|
||||
subject string,
|
||||
date time.Time,
|
||||
) (string, int64) {
|
||||
t.Helper()
|
||||
meta := event.MessageMetadata{
|
||||
Mailbox: mailbox,
|
||||
To: []*mail.Address{{Name: "Some Body", Address: "somebody@host"}},
|
||||
From: &mail.Address{Name: "Some B. Else", Address: "somebodyelse@host"},
|
||||
Subject: subject,
|
||||
Date: date,
|
||||
}
|
||||
testMsg := fmt.Sprintf("To: %s\r\nFrom: %s\r\nSubject: %s\r\n\r\nTest Body\r\n",
|
||||
meta.To[0].Address, meta.From.Address, subject)
|
||||
delivery := &message.Delivery{
|
||||
Meta: meta,
|
||||
Reader: io.NopCloser(strings.NewReader(testMsg)),
|
||||
}
|
||||
id, err := store.AddMessage(delivery)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return id, int64(len(testMsg))
|
||||
}
|
||||
|
||||
// GetAndCountMessages is a test helper that expects to receive count messages or fails the test, it
|
||||
// also checks return error.
|
||||
func GetAndCountMessages(t *testing.T, s storage.Store, mailbox string, count int) []storage.Message {
|
||||
t.Helper()
|
||||
msgs, err := s.GetMessages(mailbox)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to GetMessages for %q: %v", mailbox, err)
|
||||
}
|
||||
if len(msgs) != count {
|
||||
t.Errorf("Got %v messages for %q, want: %v", len(msgs), mailbox, count)
|
||||
}
|
||||
return msgs
|
||||
require.NoError(s, err, "VisitMailboxes() failed")
|
||||
assert.Equal(s, 5, nboxes, "visited %v mailboxes, want: 5", nboxes)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -84,7 +83,7 @@ func TestStoreStubMailboxAddGetVisit(t *testing.T) {
|
||||
|
||||
want, ok := expectCounts[mailbox]
|
||||
assert.True(t, ok, "Mailbox %q was unexpected", mailbox)
|
||||
assert.Equal(t, want, len(m), "Unexpected message count for mailbox %q", mailbox)
|
||||
assert.Len(t, m, want, "Unexpected message count for mailbox %q", mailbox)
|
||||
|
||||
delete(expectCounts, mailbox)
|
||||
|
||||
@@ -114,7 +113,7 @@ func TestStoreStubMarkSeen(t *testing.T) {
|
||||
// Mark second message as seen.
|
||||
seen := inputMsgs[1]
|
||||
err := ss.MarkSeen("box1", seen.ID())
|
||||
assert.NoError(t, err, "MarkSeen must not fail")
|
||||
require.NoError(t, err, "MarkSeen must not fail")
|
||||
|
||||
// Verify message has seen flag.
|
||||
got, err := ss.GetMessage("box1", seen.ID())
|
||||
@@ -150,16 +149,16 @@ func TestStoreStubRemoveMessage(t *testing.T) {
|
||||
// Delete second message.
|
||||
deleted := inputMsgs[1]
|
||||
err := ss.RemoveMessage("box1", deleted.ID())
|
||||
assert.NoError(t, err, "DeleteMessage must not fail")
|
||||
require.NoError(t, err, "DeleteMessage must not fail")
|
||||
|
||||
// Verify message is not in mailbox.
|
||||
messages, err := ss.GetMessages("box1")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, messages, deleted, "Mailbox should not contain msg %q", deleted.ID())
|
||||
|
||||
// Verify message is no longer retrievable.
|
||||
got, err := ss.GetMessage("box1", deleted.ID())
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, got, "Message should have been nil")
|
||||
|
||||
// Verify message is in deleted list.
|
||||
@@ -181,12 +180,12 @@ func TestStoreStubPurgeMessages(t *testing.T) {
|
||||
|
||||
// Purge messages.
|
||||
err := ss.PurgeMessages("box1")
|
||||
assert.NoError(t, err, "PurgeMessages must not fail")
|
||||
require.NoError(t, err, "PurgeMessages must not fail")
|
||||
|
||||
// Verify message is not in mailbox.
|
||||
messages, err := ss.GetMessages("box1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, messages, 0, "Mailbox should be empty")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, messages, "Mailbox should be empty")
|
||||
|
||||
// Verify messages are in deleted list.
|
||||
for _, want := range inputMsgs {
|
||||
@@ -208,15 +207,15 @@ func TestStoreStubForcedErrors(t *testing.T) {
|
||||
|
||||
// Verify methods return error.
|
||||
_, err = ss.GetMessage("messageerr", id1)
|
||||
assert.Error(t, err, "GetMessage()")
|
||||
require.Error(t, err, "GetMessage()")
|
||||
assert.NotEqual(t, storage.ErrNotExist, err)
|
||||
|
||||
_, err = ss.GetMessages("messageserr")
|
||||
assert.Error(t, err, "GetMessages()")
|
||||
require.Error(t, err, "GetMessages()")
|
||||
assert.NotEqual(t, storage.ErrNotExist, err)
|
||||
|
||||
err = ss.MarkSeen("messageerr", id1)
|
||||
assert.Error(t, err, "MarkSeen()")
|
||||
require.Error(t, err, "MarkSeen()")
|
||||
assert.NotEqual(t, storage.ErrNotExist, err)
|
||||
}
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func RootGreeting(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
||||
greeting, err := os.ReadFile(ctx.RootConfig.Web.GreetingFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to load greeting: %v", err)
|
||||
return fmt.Errorf("failed to load greeting: %v", err)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
||||
@@ -30,5 +30,4 @@ func TestSanitizeStyle(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func sanitizeStyleTags(input string) (string, error) {
|
||||
|
||||
func styleTagFilter(w io.Writer, r io.Reader) error {
|
||||
bw := bufio.NewWriter(w)
|
||||
b := make([]byte, 256)
|
||||
b := make([]byte, 0, 256)
|
||||
z := html.NewTokenizer(r)
|
||||
for {
|
||||
b = b[:0]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{ pkgs ? import <nixpkgs> { } }:
|
||||
{
|
||||
pkgs ? import <nixpkgs> { },
|
||||
}:
|
||||
let
|
||||
scripts = {
|
||||
# Quick test script.
|
||||
@@ -22,7 +24,8 @@ pkgs.mkShell {
|
||||
elmPackages.elm-json
|
||||
elmPackages.elm-language-server
|
||||
elmPackages.elm-test
|
||||
go_1_21
|
||||
go_1_25
|
||||
golangci-lint
|
||||
golint
|
||||
gopls
|
||||
nodejs_20
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0",
|
||||
"jweir/sparkline": "4.0.0",
|
||||
"ryannhg/date-format": "2.3.0"
|
||||
"ryan-haskell/date-format": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/bytes": "1.0.8",
|
||||
|
||||
38
ui/yarn.lock
38
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"
|
||||
|
||||
@@ -1035,12 +1035,12 @@ brace-expansion@^1.1.7:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
braces@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||
braces@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
fill-range "^7.1.1"
|
||||
|
||||
browserslist@^4.6.6:
|
||||
version "4.22.3"
|
||||
@@ -1340,10 +1340,10 @@ escape-string-regexp@^1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
|
||||
|
||||
fill-range@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
||||
fill-range@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
|
||||
dependencies:
|
||||
to-regex-range "^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"
|
||||
|
||||
@@ -1697,11 +1697,11 @@ mdn-data@2.0.14:
|
||||
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
|
||||
|
||||
micromatch@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
||||
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
|
||||
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
|
||||
dependencies:
|
||||
braces "^3.0.2"
|
||||
braces "^3.0.3"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
minimatch@^3.1.1:
|
||||
|
||||
Reference in New Issue
Block a user