mirror of
https://github.com/jhillyerd/inbucket.git
synced 2026-11-05 17:12:33 +00:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
@@ -10,28 +10,23 @@ jobs:
|
|||||||
linux-go-build:
|
linux-go-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Linux Go ${{ matrix.go }} build
|
name: Linux Go ${{ matrix.go }} build
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go:
|
go:
|
||||||
- '1.21'
|
- '1.23'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Build and test
|
- name: Build and test
|
||||||
run: |
|
run: |
|
||||||
go build ./...
|
go build ./...
|
||||||
go test -race -coverprofile=profile.cov ./...
|
go test -race -coverprofile=profile.cov ./...
|
||||||
|
|
||||||
- name: Send coverage
|
- name: Send coverage
|
||||||
uses: shogo82148/actions-goveralls@v1
|
uses: shogo82148/actions-goveralls@v1
|
||||||
with:
|
with:
|
||||||
@@ -42,23 +37,18 @@ jobs:
|
|||||||
windows-go-build:
|
windows-go-build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
name: Windows Go build
|
name: Windows Go build
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.23'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build ./...
|
run: go build ./...
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test -race -coverprofile="profile.cov" ./...
|
run: go test -race -coverprofile="profile.cov" ./...
|
||||||
|
|
||||||
- name: Send coverage
|
- name: Send coverage
|
||||||
uses: shogo82148/actions-goveralls@v1
|
uses: shogo82148/actions-goveralls@v1
|
||||||
with:
|
with:
|
||||||
@@ -66,13 +56,32 @@ jobs:
|
|||||||
flag-name: Windows-Go
|
flag-name: Windows-Go
|
||||||
parallel: true
|
parallel: true
|
||||||
|
|
||||||
|
ui-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: UI Build
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
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:
|
coverage:
|
||||||
needs:
|
needs:
|
||||||
- linux-go-build
|
- linux-go-build
|
||||||
- windows-go-build
|
- windows-go-build
|
||||||
name: Test Coverage
|
name: Test Coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: shogo82148/actions-goveralls@v1
|
- uses: shogo82148/actions-goveralls@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
inbucket/inbucket
|
inbucket/inbucket
|
||||||
@@ -39,30 +39,30 @@ jobs:
|
|||||||
latest=auto
|
latest=auto
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
with:
|
||||||
platforms: all
|
platforms: all
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: ${{ env.REGISTRY_PUSH == 'true' }}
|
if: ${{ env.REGISTRY_PUSH == 'true' }}
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: ${{ env.REGISTRY_PUSH == 'true' }}
|
if: ${{ env.REGISTRY_PUSH == 'true' }}
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64, linux/arm/v7
|
platforms: linux/amd64,linux/arm64, linux/arm/v7
|
||||||
|
|||||||
@@ -2,22 +2,16 @@ name: Lint Go Code
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
golangci:
|
golangci:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.23'
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
# Disable cache to prevent `File exists` errors.
|
|
||||||
# https://github.com/golangci/golangci-lint-action/issues/135
|
|
||||||
skip-pkg-cache: true
|
|
||||||
|
|||||||
@@ -6,25 +6,25 @@ on:
|
|||||||
- main
|
- main
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: 'Go Releaser'
|
name: 'Go Releaser'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.23'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -37,17 +37,19 @@ jobs:
|
|||||||
working-directory: ./ui
|
working-directory: ./ui
|
||||||
|
|
||||||
- name: Test build release
|
- name: Test build release
|
||||||
uses: goreleaser/goreleaser-action@v4
|
uses: goreleaser/goreleaser-action@v6
|
||||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||||
with:
|
with:
|
||||||
version: latest
|
distribution: goreleaser
|
||||||
|
version: "~> v2"
|
||||||
args: release --snapshot
|
args: release --snapshot
|
||||||
|
|
||||||
- name: Build and publish release
|
- name: Build and publish release
|
||||||
uses: goreleaser/goreleaser-action@v4
|
uses: goreleaser/goreleaser-action@v6
|
||||||
if: "startsWith(github.ref, 'refs/tags/v')"
|
if: "startsWith(github.ref, 'refs/tags/v')"
|
||||||
with:
|
with:
|
||||||
version: latest
|
distribution: goreleaser
|
||||||
|
version: "~> v2"
|
||||||
args: release
|
args: release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- asasalint
|
||||||
|
- asciicheck
|
||||||
|
- bidichk
|
||||||
|
- bodyclose
|
||||||
|
- containedctx
|
||||||
|
- contextcheck
|
||||||
|
- decorder
|
||||||
|
# - dupl
|
||||||
|
# - dupword
|
||||||
|
- durationcheck
|
||||||
|
- errchkjson
|
||||||
|
- errname
|
||||||
|
# - errorlint
|
||||||
|
# - exhaustive
|
||||||
|
- exportloopref
|
||||||
|
# - forcetypeassert
|
||||||
|
- ginkgolinter
|
||||||
|
- gocheckcompilerdirectives
|
||||||
|
# - gochecknoinits
|
||||||
|
- gochecksumtype
|
||||||
|
- gocritic
|
||||||
|
# - godot
|
||||||
|
# - goerr113
|
||||||
|
- gofmt
|
||||||
|
# - gofumpt
|
||||||
|
- goheader
|
||||||
|
- goimports
|
||||||
|
- gomoddirectives
|
||||||
|
- gomodguard
|
||||||
|
- goprintffuncname
|
||||||
|
# - gosec
|
||||||
|
- gosmopolitan
|
||||||
|
- grouper
|
||||||
|
- importas
|
||||||
|
- inamedparam
|
||||||
|
- interfacebloat
|
||||||
|
- loggercheck
|
||||||
|
- makezero
|
||||||
|
- mirror
|
||||||
|
- misspell
|
||||||
|
- musttag
|
||||||
|
- nilerr
|
||||||
|
# - nilnil
|
||||||
|
# - nlreturn
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- nosprintfhostport
|
||||||
|
- perfsprint
|
||||||
|
- prealloc
|
||||||
|
- predeclared
|
||||||
|
- promlinter
|
||||||
|
- protogetter
|
||||||
|
- reassign
|
||||||
|
# - revive
|
||||||
|
- rowserrcheck
|
||||||
|
- sloglint
|
||||||
|
- stylecheck
|
||||||
|
- tagliatelle
|
||||||
|
- tenv
|
||||||
|
- testableexamples
|
||||||
|
- testifylint
|
||||||
|
- thelper
|
||||||
|
- tparallel
|
||||||
|
# - unconvert
|
||||||
|
- unparam
|
||||||
|
- usestdlibvars
|
||||||
|
- wastedassign
|
||||||
|
- whitespace
|
||||||
|
- zerologlint
|
||||||
|
linters-settings:
|
||||||
|
tagliatelle:
|
||||||
|
case:
|
||||||
|
rules:
|
||||||
|
json: kebab
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
version: 2 # goreleaser version
|
||||||
project_name: inbucket
|
project_name: inbucket
|
||||||
|
|
||||||
release:
|
release:
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"runtime.version": "Lua 5.1",
|
||||||
|
"diagnostics": {
|
||||||
|
"globals": [
|
||||||
|
"inbucket",
|
||||||
|
"smtp"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-4
@@ -4,6 +4,26 @@ Change Log
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
|
||||||
|
## [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
|
## [v3.1.0-beta2] - 2024-02-05
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -350,7 +370,8 @@ No change from beta1.
|
|||||||
specific message.
|
specific message.
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/inbucket/inbucket/compare/v3.1.0-beta2...main
|
[Unreleased]: https://github.com/inbucket/inbucket/compare/v3.1.0-beta3...main
|
||||||
|
[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-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.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
|
[v3.0.4]: https://github.com/inbucket/inbucket/compare/v3.0.3...v3.0.4
|
||||||
@@ -389,9 +410,10 @@ No change from beta1.
|
|||||||
- Update previous tag version for *Unreleased*
|
- Update previous tag version for *Unreleased*
|
||||||
3. Run tests
|
3. Run tests
|
||||||
4. Update goreleaser, and then test cross-compile: `goreleaser --snapshot`
|
4. Update goreleaser, and then test cross-compile: `goreleaser --snapshot`
|
||||||
5. Commit changes and merge release into main, tag `vX.Y.Z`
|
5. Commit changes and merge release PR into main
|
||||||
6. Push tags and wait for
|
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
|
[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.
|
See http://keepachangelog.com/ for additional instructions on how to update this file.
|
||||||
|
|||||||
+13
-13
@@ -12,18 +12,18 @@ RUN yarn install --frozen-lockfile --non-interactive
|
|||||||
RUN yarn run build
|
RUN yarn run build
|
||||||
|
|
||||||
### Build backend
|
### Build backend
|
||||||
FROM golang:1.21-alpine3.19 as backend
|
FROM golang:1.23-alpine3.20 as backend
|
||||||
RUN apk add --no-cache --virtual .build-deps g++ git make
|
RUN apk add --no-cache --virtual .build-deps g++ git make
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV CGO_ENABLED 0
|
ENV CGO_ENABLED=0
|
||||||
RUN make clean deps
|
RUN make clean deps
|
||||||
RUN go build -o inbucket \
|
RUN go build -o inbucket \
|
||||||
-ldflags "-X 'main.version=$(git describe --tags --always)' -X 'main.date=$(date -Iseconds)'" \
|
-ldflags "-X 'main.version=$(git describe --tags --always)' -X 'main.date=$(date -Iseconds)'" \
|
||||||
-v ./cmd/inbucket
|
-v ./cmd/inbucket
|
||||||
|
|
||||||
### Run in minimal image
|
### Run in minimal image
|
||||||
FROM alpine:3.19
|
FROM alpine:3.20
|
||||||
RUN apk --no-cache add tzdata
|
RUN apk --no-cache add tzdata
|
||||||
WORKDIR /opt/inbucket
|
WORKDIR /opt/inbucket
|
||||||
RUN mkdir bin defaults ui
|
RUN mkdir bin defaults ui
|
||||||
@@ -33,16 +33,16 @@ COPY etc/docker/defaults/greeting.html defaults
|
|||||||
COPY etc/docker/defaults/start-inbucket.sh /
|
COPY etc/docker/defaults/start-inbucket.sh /
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
ENV INBUCKET_SMTP_DISCARDDOMAINS bitbucket.local
|
ENV INBUCKET_SMTP_DISCARDDOMAINS=bitbucket.local
|
||||||
ENV INBUCKET_SMTP_TIMEOUT 30s
|
ENV INBUCKET_SMTP_TIMEOUT=30s
|
||||||
ENV INBUCKET_POP3_TIMEOUT 30s
|
ENV INBUCKET_POP3_TIMEOUT=30s
|
||||||
ENV INBUCKET_WEB_GREETINGFILE /config/greeting.html
|
ENV INBUCKET_WEB_GREETINGFILE=/config/greeting.html
|
||||||
ENV INBUCKET_WEB_COOKIEAUTHKEY secret-inbucket-session-cookie-key
|
ENV INBUCKET_WEB_COOKIEAUTHKEY=secret-inbucket-session-cookie-key
|
||||||
ENV INBUCKET_WEB_UIDIR ui
|
ENV INBUCKET_WEB_UIDIR=ui
|
||||||
ENV INBUCKET_STORAGE_TYPE file
|
ENV INBUCKET_STORAGE_TYPE=file
|
||||||
ENV INBUCKET_STORAGE_PARAMS path:/storage
|
ENV INBUCKET_STORAGE_PARAMS=path:/storage
|
||||||
ENV INBUCKET_STORAGE_RETENTIONPERIOD 72h
|
ENV INBUCKET_STORAGE_RETENTIONPERIOD=72h
|
||||||
ENV INBUCKET_STORAGE_MAILBOXMSGCAP 300
|
ENV INBUCKET_STORAGE_MAILBOXMSGCAP=300
|
||||||
|
|
||||||
# Healthcheck
|
# 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'
|
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'
|
||||||
|
|||||||
+4
-2
@@ -28,18 +28,20 @@ func (*listCmd) Usage() string {
|
|||||||
func (l *listCmd) SetFlags(f *flag.FlagSet) {}
|
func (l *listCmd) SetFlags(f *flag.FlagSet) {}
|
||||||
|
|
||||||
func (l *listCmd) Execute(
|
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)
|
mailbox := f.Arg(0)
|
||||||
if mailbox == "" {
|
if mailbox == "" {
|
||||||
return usage("mailbox required")
|
return usage("mailbox required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup rest client
|
// Setup rest client
|
||||||
c, err := client.New(baseURL())
|
c, err := client.New(baseURL())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatal("Couldn't build client", err)
|
return fatal("Couldn't build client", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get list
|
// Get list
|
||||||
headers, err := c.ListMailbox(mailbox)
|
headers, err := c.ListMailboxWithContext(ctx, mailbox)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatal("REST call failed", err)
|
return fatal("REST call failed", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -5,8 +5,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/google/subcommands"
|
"github.com/google/subcommands"
|
||||||
)
|
)
|
||||||
@@ -68,7 +70,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func baseURL() string {
|
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 {
|
func fatal(msg string, err error) subcommands.ExitStatus {
|
||||||
|
|||||||
+16
-8
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
type matchCmd struct {
|
type matchCmd struct {
|
||||||
output string
|
output string
|
||||||
outFunc func(headers []*client.MessageHeader) error
|
outFunc func(ctx context.Context, headers []*client.MessageHeader) error
|
||||||
delete bool
|
delete bool
|
||||||
// match criteria
|
// match criteria
|
||||||
from regexFlag
|
from regexFlag
|
||||||
@@ -51,11 +51,12 @@ func (m *matchCmd) SetFlags(f *flag.FlagSet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *matchCmd) Execute(
|
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)
|
mailbox := f.Arg(0)
|
||||||
if mailbox == "" {
|
if mailbox == "" {
|
||||||
return usage("mailbox required")
|
return usage("mailbox required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select output function
|
// Select output function
|
||||||
switch m.output {
|
switch m.output {
|
||||||
case "id":
|
case "id":
|
||||||
@@ -67,16 +68,19 @@ func (m *matchCmd) Execute(
|
|||||||
default:
|
default:
|
||||||
return usage("unknown output type: " + m.output)
|
return usage("unknown output type: " + m.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup REST client
|
// Setup REST client
|
||||||
c, err := client.New(baseURL())
|
c, err := client.New(baseURL())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatal("Couldn't build client", err)
|
return fatal("Couldn't build client", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get list
|
// Get list
|
||||||
headers, err := c.ListMailbox(mailbox)
|
headers, err := c.ListMailboxWithContext(ctx, mailbox)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatal("List REST call failed", err)
|
return fatal("List REST call failed", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find matches
|
// Find matches
|
||||||
matches := make([]*client.MessageHeader, 0, len(headers))
|
matches := make([]*client.MessageHeader, 0, len(headers))
|
||||||
for _, h := range headers {
|
for _, h := range headers {
|
||||||
@@ -84,24 +88,28 @@ func (m *matchCmd) Execute(
|
|||||||
matches = append(matches, h)
|
matches = append(matches, h)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return error status if no matches
|
// Return error status if no matches
|
||||||
if len(matches) == 0 {
|
if len(matches) == 0 {
|
||||||
return subcommands.ExitFailure
|
return subcommands.ExitFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output matches
|
// Output matches
|
||||||
err = m.outFunc(matches)
|
err = m.outFunc(ctx, matches)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatal("Error", err)
|
return fatal("Error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optionally, delete matches
|
||||||
if m.delete {
|
if m.delete {
|
||||||
// Delete matches
|
|
||||||
for _, h := range matches {
|
for _, h := range matches {
|
||||||
err = h.Delete()
|
err = h.DeleteWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatal("Delete REST call failed", err)
|
return fatal("Delete REST call failed", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return subcommands.ExitSuccess
|
return subcommands.ExitSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,14 +156,14 @@ func (m *matchCmd) match(header *client.MessageHeader) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func outputID(headers []*client.MessageHeader) error {
|
func outputID(_ context.Context, headers []*client.MessageHeader) error {
|
||||||
for _, h := range headers {
|
for _, h := range headers {
|
||||||
fmt.Println(h.ID)
|
fmt.Println(h.ID)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func outputJSON(headers []*client.MessageHeader) error {
|
func outputJSON(_ context.Context, headers []*client.MessageHeader) error {
|
||||||
jsonEncoder := json.NewEncoder(os.Stdout)
|
jsonEncoder := json.NewEncoder(os.Stdout)
|
||||||
jsonEncoder.SetEscapeHTML(false)
|
jsonEncoder.SetEscapeHTML(false)
|
||||||
jsonEncoder.SetIndent("", " ")
|
jsonEncoder.SetIndent("", " ")
|
||||||
|
|||||||
+14
-10
@@ -33,44 +33,48 @@ func (m *mboxCmd) SetFlags(f *flag.FlagSet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *mboxCmd) Execute(
|
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)
|
mailbox := f.Arg(0)
|
||||||
if mailbox == "" {
|
if mailbox == "" {
|
||||||
return usage("mailbox required")
|
return usage("mailbox required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup REST client
|
// Setup REST client
|
||||||
c, err := client.New(baseURL())
|
c, err := client.New(baseURL())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatal("Couldn't build client", err)
|
return fatal("Couldn't build client", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get list
|
// Get list
|
||||||
headers, err := c.ListMailbox(mailbox)
|
headers, err := c.ListMailboxWithContext(ctx, mailbox)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatal("List REST call failed", err)
|
return fatal("List REST call failed", err)
|
||||||
}
|
}
|
||||||
err = outputMbox(headers)
|
err = outputMbox(ctx, headers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatal("Error", err)
|
return fatal("Error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optionally, delete retrieved messages
|
||||||
if m.delete {
|
if m.delete {
|
||||||
// Delete matches
|
|
||||||
for _, h := range headers {
|
for _, h := range headers {
|
||||||
err = h.Delete()
|
err = h.DeleteWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatal("Delete REST call failed", err)
|
return fatal("Delete REST call failed", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return subcommands.ExitSuccess
|
return subcommands.ExitSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
// outputMbox renders messages in mbox format
|
// outputMbox renders messages in mbox format.
|
||||||
// also used by match subcommand
|
// It is also used by match subcommand.
|
||||||
func outputMbox(headers []*client.MessageHeader) error {
|
func outputMbox(ctx context.Context, headers []*client.MessageHeader) error {
|
||||||
for _, h := range headers {
|
for _, h := range headers {
|
||||||
source, err := h.GetSource()
|
source, err := h.GetSourceWithContext(ctx)
|
||||||
if err != nil {
|
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)
|
fmt.Printf("From %s\n", h.From)
|
||||||
|
|||||||
+10
-6
@@ -162,7 +162,7 @@ signalLoop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// openLog configures zerolog output, returns func to close logfile.
|
// 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 {
|
switch level {
|
||||||
case "debug":
|
case "debug":
|
||||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
@@ -173,9 +173,10 @@ func openLog(level string, logfile string, json bool) (close func(), err error)
|
|||||||
case "error":
|
case "error":
|
||||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
default:
|
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
|
var w io.Writer
|
||||||
color := runtime.GOOS != "windows"
|
color := runtime.GOOS != "windows"
|
||||||
switch logfile {
|
switch logfile {
|
||||||
@@ -191,21 +192,24 @@ func openLog(level string, logfile string, json bool) (close func(), err error)
|
|||||||
bw := bufio.NewWriter(logf)
|
bw := bufio.NewWriter(logf)
|
||||||
w = bw
|
w = bw
|
||||||
color = false
|
color = false
|
||||||
close = func() {
|
closeLog = func() {
|
||||||
_ = bw.Flush()
|
_ = bw.Flush()
|
||||||
_ = logf.Close()
|
_ = logf.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w = zerolog.SyncWriter(w)
|
w = zerolog.SyncWriter(w)
|
||||||
if json {
|
if json {
|
||||||
log.Logger = log.Output(w)
|
log.Logger = log.Output(w)
|
||||||
return close, nil
|
return closeLog, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{
|
log.Logger = log.Output(zerolog.ConsoleWriter{
|
||||||
Out: w,
|
Out: w,
|
||||||
NoColor: !color,
|
NoColor: !color,
|
||||||
})
|
})
|
||||||
return close, nil
|
|
||||||
|
return closeLog, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// removePIDFile removes the PID file if created.
|
// removePIDFile removes the PID file if created.
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ require (
|
|||||||
github.com/google/subcommands v1.2.0
|
github.com/google/subcommands v1.2.0
|
||||||
github.com/gorilla/css v1.0.1
|
github.com/gorilla/css v1.0.1
|
||||||
github.com/gorilla/mux v1.8.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/inbucket/gopher-json v0.2.0
|
||||||
github.com/jhillyerd/enmime v1.1.0
|
github.com/jhillyerd/enmime/v2 v2.0.0
|
||||||
github.com/jhillyerd/goldiff v0.1.0
|
github.com/jhillyerd/goldiff v0.1.0
|
||||||
github.com/kelseyhightower/envconfig v1.4.0
|
github.com/kelseyhightower/envconfig v1.4.0
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26
|
github.com/microcosm-cc/bluemonday v1.0.27
|
||||||
github.com/rs/zerolog v1.32.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/yuin/gopher-lua v1.1.1
|
github.com/yuin/gopher-lua v1.1.1
|
||||||
golang.org/x/net v0.20.0
|
golang.org/x/net v0.29.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -31,16 +31,16 @@ require (
|
|||||||
github.com/kr/pretty v0.3.1 // 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.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // 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/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // 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/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 // indirect
|
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 // indirect
|
||||||
golang.org/x/sys v0.16.0 // indirect
|
golang.org/x/sys v0.25.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.18.0 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
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/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 h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
|
||||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
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/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 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
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.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
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 h1:v/luoFy5olitFhByVUGMZ3LmtcroRs9YHlyrBedz7EA=
|
||||||
github.com/inbucket/gopher-json v0.2.0/go.mod h1:1BK2XgU9y+ibiRkylJQeV44AV9DrO8dVsgOJ6vpqF3g=
|
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 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
|
||||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
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/v2 v2.0.0 h1:I39PYf0peLGroKq+uX2yGB1ExH/78HcRJy4VmERQAVk=
|
||||||
github.com/jhillyerd/enmime v1.1.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
|
github.com/jhillyerd/enmime/v2 v2.0.0/go.mod h1:wQkz7BochDzSukAz5ajAQaXOB7pEg5Vh5QWs7m1uAPw=
|
||||||
github.com/jhillyerd/goldiff v0.1.0 h1:7JzKPKVwAg1GzrbnsToYzq3Y5+S7dXM4hgEYiOzaf4A=
|
github.com/jhillyerd/goldiff v0.1.0 h1:7JzKPKVwAg1GzrbnsToYzq3Y5+S7dXM4hgEYiOzaf4A=
|
||||||
github.com/jhillyerd/goldiff v0.1.0/go.mod h1:WeDal6DTqhbMhNkf5REzWCIvKl3JWs0Q9omZ/huIWAs=
|
github.com/jhillyerd/goldiff v0.1.0/go.mod h1:WeDal6DTqhbMhNkf5REzWCIvKl3JWs0Q9omZ/huIWAs=
|
||||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||||
@@ -47,10 +47,10 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
|||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
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-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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
@@ -61,30 +61,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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
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 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
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/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
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/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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.9.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 h1:noHsffKZsNfU38DwcXWEPldrTjIZ8FPNKx8mYMGnqjs=
|
||||||
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7/go.mod h1:bbMEM6aU1WDF1ErA5YJ0p91652pGv140gGw4Ww3RGp8=
|
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 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
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.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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":
|
case "domain":
|
||||||
*n = DomainNaming
|
*n = DomainNaming
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("Unknown MailboxNaming strategy: %q", v)
|
return fmt.Errorf("unknown MailboxNaming strategy: %q", v)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func (eb *AsyncEventBroker[E]) AsyncTestListener(name string, capacity int) func
|
|||||||
return &event, nil
|
return &event, nil
|
||||||
|
|
||||||
case <-time.After(time.Second * 2):
|
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"
|
want := "hi"
|
||||||
broker.Emit(&want)
|
broker.Emit(&want)
|
||||||
|
|
||||||
first_got, err := first()
|
firstGot, err := first()
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, want, *second_got)
|
assert.Equal(t, want, *secondGot)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAsyncBrokerAddingDuplicateNameReplacesPrevious(t *testing.T) {
|
func TestAsyncBrokerAddingDuplicateNameReplacesPrevious(t *testing.T) {
|
||||||
@@ -66,13 +66,13 @@ func TestAsyncBrokerAddingDuplicateNameReplacesPrevious(t *testing.T) {
|
|||||||
want := "hi"
|
want := "hi"
|
||||||
broker.Emit(&want)
|
broker.Emit(&want)
|
||||||
|
|
||||||
first_got, err := first()
|
firstGot, err := first()
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Nil(t, first_got)
|
assert.Nil(t, firstGot)
|
||||||
|
|
||||||
second_got, err := second()
|
secondGot, err := second()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, want, *second_got)
|
assert.Equal(t, want, *secondGot)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAsyncBrokerRemovingListenerSuccessful(t *testing.T) {
|
func TestAsyncBrokerRemovingListenerSuccessful(t *testing.T) {
|
||||||
@@ -86,13 +86,13 @@ func TestAsyncBrokerRemovingListenerSuccessful(t *testing.T) {
|
|||||||
want := "hi"
|
want := "hi"
|
||||||
broker.Emit(&want)
|
broker.Emit(&want)
|
||||||
|
|
||||||
first_got, err := first()
|
firstGot, err := first()
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Nil(t, first_got)
|
assert.Nil(t, firstGot)
|
||||||
|
|
||||||
second_got, err := second()
|
secondGot, err := second()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, want, *second_got)
|
assert.Equal(t, want, *secondGot)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAsyncBrokerRemovingMissingListener(t *testing.T) {
|
func TestAsyncBrokerRemovingMissingListener(t *testing.T) {
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ func TestBrokerEmitCallsMultipleListeners(t *testing.T) {
|
|||||||
broker := &extension.EventBroker[string, bool]{}
|
broker := &extension.EventBroker[string, bool]{}
|
||||||
|
|
||||||
// Setup listeners.
|
// Setup listeners.
|
||||||
var first_got, second_got string
|
var firstGot, secondGot string
|
||||||
first := func(s string) *bool {
|
first := func(s string) *bool {
|
||||||
first_got = s
|
firstGot = s
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
second := func(s string) *bool {
|
second := func(s string) *bool {
|
||||||
second_got = s
|
secondGot = s
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,11 +43,11 @@ func TestBrokerEmitCallsMultipleListeners(t *testing.T) {
|
|||||||
|
|
||||||
want := "hi"
|
want := "hi"
|
||||||
broker.Emit(&want)
|
broker.Emit(&want)
|
||||||
if first_got != want {
|
if firstGot != want {
|
||||||
t.Errorf("first got %q, want %q", first_got, want)
|
t.Errorf("first got %q, want %q", firstGot, want)
|
||||||
}
|
}
|
||||||
if second_got != want {
|
if secondGot != want {
|
||||||
t.Errorf("second got %q, want %q", second_got, want)
|
t.Errorf("second got %q, want %q", secondGot, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,13 +77,13 @@ func TestBrokerAddingDuplicateNameReplacesPrevious(t *testing.T) {
|
|||||||
broker := &extension.EventBroker[string, bool]{}
|
broker := &extension.EventBroker[string, bool]{}
|
||||||
|
|
||||||
// Setup listeners.
|
// Setup listeners.
|
||||||
var first_got, second_got string
|
var firstGot, secondGot string
|
||||||
first := func(s string) *bool {
|
first := func(s string) *bool {
|
||||||
first_got = s
|
firstGot = s
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
second := func(s string) *bool {
|
second := func(s string) *bool {
|
||||||
second_got = s
|
secondGot = s
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,11 +92,11 @@ func TestBrokerAddingDuplicateNameReplacesPrevious(t *testing.T) {
|
|||||||
|
|
||||||
want := "hi"
|
want := "hi"
|
||||||
broker.Emit(&want)
|
broker.Emit(&want)
|
||||||
if first_got != "" {
|
if firstGot != "" {
|
||||||
t.Errorf("first got %q, want empty string", first_got)
|
t.Errorf("first got %q, want empty string", firstGot)
|
||||||
}
|
}
|
||||||
if second_got != want {
|
if secondGot != want {
|
||||||
t.Errorf("second got %q, want %q", second_got, want)
|
t.Errorf("second got %q, want %q", secondGot, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,13 +104,13 @@ func TestBrokerRemovingListenerSuccessful(t *testing.T) {
|
|||||||
broker := &extension.EventBroker[string, bool]{}
|
broker := &extension.EventBroker[string, bool]{}
|
||||||
|
|
||||||
// Setup listeners.
|
// Setup listeners.
|
||||||
var first_got, second_got string
|
var firstGot, secondGot string
|
||||||
first := func(s string) *bool {
|
first := func(s string) *bool {
|
||||||
first_got = s
|
firstGot = s
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
second := func(s string) *bool {
|
second := func(s string) *bool {
|
||||||
second_got = s
|
secondGot = s
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,11 +120,11 @@ func TestBrokerRemovingListenerSuccessful(t *testing.T) {
|
|||||||
|
|
||||||
want := "hi"
|
want := "hi"
|
||||||
broker.Emit(&want)
|
broker.Emit(&want)
|
||||||
if first_got != "" {
|
if firstGot != "" {
|
||||||
t.Errorf("first got %q, want empty string", first_got)
|
t.Errorf("first got %q, want empty string", firstGot)
|
||||||
}
|
}
|
||||||
if second_got != want {
|
if secondGot != want {
|
||||||
t.Errorf("second got %q, want %q", second_got, want)
|
t.Errorf("second got %q, want %q", secondGot, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ import (
|
|||||||
"time"
|
"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.
|
// AddressParts contains the local and domain parts of an email address.
|
||||||
type AddressParts struct {
|
type AddressParts struct {
|
||||||
Local string
|
Local string
|
||||||
@@ -31,3 +40,17 @@ type MessageMetadata struct {
|
|||||||
Size int64
|
Size int64
|
||||||
Seen bool
|
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
|
// 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
|
// 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 {
|
type Events struct {
|
||||||
AfterMessageDeleted AsyncEventBroker[event.MessageMetadata]
|
AfterMessageDeleted AsyncEventBroker[event.MessageMetadata]
|
||||||
AfterMessageStored AsyncEventBroker[event.MessageMetadata]
|
AfterMessageStored AsyncEventBroker[event.MessageMetadata]
|
||||||
BeforeMailAccepted EventBroker[event.AddressParts, bool]
|
BeforeMailFromAccepted EventBroker[event.SMTPSession, event.SMTPResponse]
|
||||||
BeforeMessageStored EventBroker[event.InboundMessage, event.InboundMessage]
|
BeforeMessageStored EventBroker[event.InboundMessage, event.InboundMessage]
|
||||||
|
BeforeRcptToAccepted EventBroker[event.SMTPSession, event.SMTPResponse]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Void indicates the event emitter will ignore any value returned by listeners.
|
// Void indicates the event emitter will ignore any value returned by listeners.
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"net/mail"
|
"net/mail"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
lua "github.com/yuin/gopher-lua"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMailAddressGetters(t *testing.T) {
|
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))
|
assert(got == want, string.format("got address %q, want %q", got, want))
|
||||||
`
|
`
|
||||||
|
|
||||||
ls := lua.NewState()
|
ls, _ := test.NewLuaState()
|
||||||
registerMailAddressType(ls)
|
registerMailAddressType(ls)
|
||||||
|
|
||||||
ls.SetGlobal("addr", wrapMailAddress(ls, want))
|
ls.SetGlobal("addr", wrapMailAddress(ls, want))
|
||||||
require.NoError(t, ls.DoString(script))
|
require.NoError(t, ls.DoString(script))
|
||||||
}
|
}
|
||||||
@@ -44,9 +45,10 @@ func TestMailAddressSetters(t *testing.T) {
|
|||||||
addr.address = "ri@example.com"
|
addr.address = "ri@example.com"
|
||||||
`
|
`
|
||||||
|
|
||||||
got := &mail.Address{}
|
ls, _ := test.NewLuaState()
|
||||||
ls := lua.NewState()
|
|
||||||
registerMailAddressType(ls)
|
registerMailAddressType(ls)
|
||||||
|
|
||||||
|
got := &mail.Address{}
|
||||||
ls.SetGlobal("addr", wrapMailAddress(ls, got))
|
ls.SetGlobal("addr", wrapMailAddress(ls, got))
|
||||||
require.NoError(t, ls.DoString(script))
|
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,
|
// Gets a field value from InboundMessage user object. This emulates a Lua table,
|
||||||
|
|||||||
@@ -5,29 +5,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||||
|
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func TestInboundMessageGetters(t *testing.T) {
|
||||||
want := &event.InboundMessage{
|
want := &event.InboundMessage{
|
||||||
Mailboxes: []string{"mb1", "mb2"},
|
Mailboxes: []string{"mb1", "mb2"},
|
||||||
@@ -44,23 +26,23 @@ func TestInboundMessageGetters(t *testing.T) {
|
|||||||
|
|
||||||
assert_eq(msg.mailboxes, {"mb1", "mb2"})
|
assert_eq(msg.mailboxes, {"mb1", "mb2"})
|
||||||
assert_eq(msg.subject, "subj1")
|
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.name, "name1", "from.name")
|
||||||
assert_eq(msg.from.address, "addr1")
|
assert_eq(msg.from.address, "addr1", "from.address")
|
||||||
|
|
||||||
assert_eq(#msg.to, 2)
|
assert_eq(#msg.to, 2, "#msg.to")
|
||||||
assert_eq(msg.to[1].name, "name2")
|
assert_eq(msg.to[1].name, "name2", "to[1].name")
|
||||||
assert_eq(msg.to[1].address, "addr2")
|
assert_eq(msg.to[1].address, "addr2", "to[1].address")
|
||||||
assert_eq(msg.to[2].name, "name3")
|
assert_eq(msg.to[2].name, "name3", "to[2].name")
|
||||||
assert_eq(msg.to[2].address, "addr3")
|
assert_eq(msg.to[2].address, "addr3", "to[2].address")
|
||||||
`
|
`
|
||||||
|
|
||||||
ls := lua.NewState()
|
ls, _ := test.NewLuaState()
|
||||||
registerInboundMessageType(ls)
|
registerInboundMessageType(ls)
|
||||||
registerMailAddressType(ls)
|
registerMailAddressType(ls)
|
||||||
ls.SetGlobal("msg", wrapInboundMessage(ls, want))
|
ls.SetGlobal("msg", wrapInboundMessage(ls, want))
|
||||||
require.NoError(t, ls.DoString(LuaInit+script))
|
require.NoError(t, ls.DoString(script))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInboundMessageSetters(t *testing.T) {
|
func TestInboundMessageSetters(t *testing.T) {
|
||||||
@@ -83,7 +65,7 @@ func TestInboundMessageSetters(t *testing.T) {
|
|||||||
`
|
`
|
||||||
|
|
||||||
got := &event.InboundMessage{}
|
got := &event.InboundMessage{}
|
||||||
ls := lua.NewState()
|
ls, _ := test.NewLuaState()
|
||||||
registerInboundMessageType(ls)
|
registerInboundMessageType(ls)
|
||||||
registerMailAddressType(ls)
|
registerMailAddressType(ls)
|
||||||
ls.SetGlobal("msg", wrapInboundMessage(ls, got))
|
ls.SetGlobal("msg", wrapInboundMessage(ls, got))
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ type InbucketAfterFuncs struct {
|
|||||||
// InbucketBeforeFuncs holds references to Lua extension functions to be called
|
// InbucketBeforeFuncs holds references to Lua extension functions to be called
|
||||||
// before Inbucket handles an event.
|
// before Inbucket handles an event.
|
||||||
type InbucketBeforeFuncs struct {
|
type InbucketBeforeFuncs struct {
|
||||||
MailAccepted *lua.LFunction
|
MailFromAccepted *lua.LFunction
|
||||||
MessageStored *lua.LFunction
|
MessageStored *lua.LFunction
|
||||||
|
RcptToAccepted *lua.LFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerInbucketTypes(ls *lua.LState) {
|
func registerInbucketTypes(ls *lua.LState) {
|
||||||
@@ -185,10 +186,12 @@ func inbucketBeforeIndex(ls *lua.LState) int {
|
|||||||
|
|
||||||
// Push the requested field's value onto the stack.
|
// Push the requested field's value onto the stack.
|
||||||
switch field {
|
switch field {
|
||||||
case "mail_accepted":
|
case "mail_from_accepted":
|
||||||
ls.Push(funcOrNil(before.MailAccepted))
|
ls.Push(funcOrNil(before.MailFromAccepted))
|
||||||
case "message_stored":
|
case "message_stored":
|
||||||
ls.Push(funcOrNil(before.MessageStored))
|
ls.Push(funcOrNil(before.MessageStored))
|
||||||
|
case "rcpt_to_accepted":
|
||||||
|
ls.Push(funcOrNil(before.RcptToAccepted))
|
||||||
default:
|
default:
|
||||||
// Unknown field.
|
// Unknown field.
|
||||||
ls.Push(lua.LNil)
|
ls.Push(lua.LNil)
|
||||||
@@ -203,10 +206,12 @@ func inbucketBeforeNewIndex(ls *lua.LState) int {
|
|||||||
index := ls.CheckString(2)
|
index := ls.CheckString(2)
|
||||||
|
|
||||||
switch index {
|
switch index {
|
||||||
case "mail_accepted":
|
case "mail_from_accepted":
|
||||||
m.MailAccepted = ls.CheckFunction(3)
|
m.MailFromAccepted = ls.CheckFunction(3)
|
||||||
case "message_stored":
|
case "message_stored":
|
||||||
m.MessageStored = ls.CheckFunction(3)
|
m.MessageStored = ls.CheckFunction(3)
|
||||||
|
case "rcpt_to_accepted":
|
||||||
|
m.RcptToAccepted = ls.CheckFunction(3)
|
||||||
default:
|
default:
|
||||||
ls.RaiseError("invalid inbucket.before index %q", index)
|
ls.RaiseError("invalid inbucket.before index %q", index)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package luahost
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
lua "github.com/yuin/gopher-lua"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInbucketAfterFuncs(t *testing.T) {
|
func TestInbucketAfterFuncs(t *testing.T) {
|
||||||
@@ -49,7 +49,7 @@ func TestInbucketAfterFuncs(t *testing.T) {
|
|||||||
end
|
end
|
||||||
`
|
`
|
||||||
|
|
||||||
ls := lua.NewState()
|
ls, _ := test.NewLuaState()
|
||||||
registerInbucketTypes(ls)
|
registerInbucketTypes(ls)
|
||||||
require.NoError(t, ls.DoString(script))
|
require.NoError(t, ls.DoString(script))
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ func TestInbucketBeforeFuncs(t *testing.T) {
|
|||||||
assert(inbucket, "inbucket should not be nil")
|
assert(inbucket, "inbucket should not be nil")
|
||||||
assert(inbucket.before, "inbucket.before 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.
|
-- Verify functions start off nil.
|
||||||
for i, name in ipairs(fns) do
|
for i, name in ipairs(fns) do
|
||||||
@@ -96,7 +96,7 @@ func TestInbucketBeforeFuncs(t *testing.T) {
|
|||||||
end
|
end
|
||||||
`
|
`
|
||||||
|
|
||||||
ls := lua.NewState()
|
ls, _ := test.NewLuaState()
|
||||||
registerInbucketTypes(ls)
|
registerInbucketTypes(ls)
|
||||||
require.NoError(t, ls.DoString(script))
|
require.NoError(t, ls.DoString(script))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||||
|
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
lua "github.com/yuin/gopher-lua"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMessageMetadataGetters(t *testing.T) {
|
func TestMessageMetadataGetters(t *testing.T) {
|
||||||
@@ -24,26 +24,22 @@ func TestMessageMetadataGetters(t *testing.T) {
|
|||||||
script := `
|
script := `
|
||||||
assert(msg, "msg should not be nil")
|
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.mailbox, "mb1")
|
||||||
assert_eq(msg.id, "id1")
|
assert_eq(msg.id, "id1")
|
||||||
assert_eq(msg.subject, "subj1")
|
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.name, "name1", "from.name")
|
||||||
assert_eq(msg.from.address, "addr1")
|
assert_eq(msg.from.address, "addr1", "from.address")
|
||||||
|
|
||||||
assert_eq(table.getn(msg.to), 1)
|
assert_eq(table.getn(msg.to), 1)
|
||||||
assert_eq(msg.to[1].name, "name2")
|
assert_eq(msg.to[1].name, "name2", "to.name")
|
||||||
assert_eq(msg.to[1].address, "addr2")
|
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)
|
registerMessageMetadataType(ls)
|
||||||
registerMailAddressType(ls)
|
registerMailAddressType(ls)
|
||||||
ls.SetGlobal("msg", wrapMessageMetadata(ls, want))
|
ls.SetGlobal("msg", wrapMessageMetadata(ls, want))
|
||||||
@@ -75,7 +71,7 @@ func TestMessageMetadataSetters(t *testing.T) {
|
|||||||
`
|
`
|
||||||
|
|
||||||
got := &event.MessageMetadata{}
|
got := &event.MessageMetadata{}
|
||||||
ls := lua.NewState()
|
ls, _ := test.NewLuaState()
|
||||||
registerMessageMetadataType(ls)
|
registerMessageMetadataType(ls)
|
||||||
registerMailAddressType(ls)
|
registerMailAddressType(ls)
|
||||||
ls.SetGlobal("msg", wrapMessageMetadata(ls, got))
|
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)
|
|
||||||
}
|
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -15,6 +16,9 @@ import (
|
|||||||
"github.com/yuin/gopher-lua/parse"
|
"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.
|
// Host of Lua extensions.
|
||||||
type Host struct {
|
type Host struct {
|
||||||
extHost *extension.Host
|
extHost *extension.Host
|
||||||
@@ -35,9 +39,9 @@ func New(conf config.Lua, extHost *extension.Host) (*Host, error) {
|
|||||||
// Pre-load, parse, and compile script.
|
// Pre-load, parse, and compile script.
|
||||||
if fi, err := os.Stat(scriptPath); err != nil {
|
if fi, err := os.Stat(scriptPath); err != nil {
|
||||||
logger.Info().Msg("Script file not found")
|
logger.Info().Msg("Script file not found")
|
||||||
return nil, nil
|
return nil, ErrNoScript
|
||||||
} else if fi.IsDir() {
|
} 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")
|
logger.Info().Msg("Loading script")
|
||||||
@@ -102,12 +106,15 @@ func (h *Host) wireFunctions(logger zerolog.Logger, ls *lua.LState) {
|
|||||||
if ib.After.MessageStored != nil {
|
if ib.After.MessageStored != nil {
|
||||||
events.AfterMessageStored.AddListener(listenerName, h.handleAfterMessageStored)
|
events.AfterMessageStored.AddListener(listenerName, h.handleAfterMessageStored)
|
||||||
}
|
}
|
||||||
if ib.Before.MailAccepted != nil {
|
if ib.Before.MailFromAccepted != nil {
|
||||||
events.BeforeMailAccepted.AddListener(listenerName, h.handleBeforeMailAccepted)
|
events.BeforeMailFromAccepted.AddListener(listenerName, h.handleBeforeMailFromAccepted)
|
||||||
}
|
}
|
||||||
if ib.Before.MessageStored != nil {
|
if ib.Before.MessageStored != nil {
|
||||||
events.BeforeMessageStored.AddListener(listenerName, h.handleBeforeMessageStored)
|
events.BeforeMessageStored.AddListener(listenerName, h.handleBeforeMessageStored)
|
||||||
}
|
}
|
||||||
|
if ib.Before.RcptToAccepted != nil {
|
||||||
|
events.BeforeRcptToAccepted.AddListener(listenerName, h.handleBeforeRcptToAccepted)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Host) handleAfterMessageDeleted(msg event.MessageMetadata) {
|
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 {
|
func (h *Host) handleBeforeMailFromAccepted(session event.SMTPSession) *event.SMTPResponse {
|
||||||
logger, ls, ib, ok := h.prepareInbucketFuncCall("before.mail_accepted")
|
logger, ls, ib, ok := h.prepareInbucketFuncCall("before.mail_from_accepted")
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
defer h.pool.putState(ls)
|
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(
|
if err := ls.CallByParam(
|
||||||
lua.P{Fn: ib.Before.MailAccepted, NRet: 1, Protect: true},
|
lua.P{Fn: ib.Before.MailFromAccepted, NRet: 1, Protect: true},
|
||||||
lua.LString(addr.Local),
|
wrapSMTPSession(ls, &session),
|
||||||
lua.LString(addr.Domain),
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.Error().Err(err).Msg("Failed to call Lua function")
|
logger.Error().Err(err).Msg("Failed to call Lua function")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
lval := ls.Get(1)
|
lval := ls.Get(-1)
|
||||||
ls.Pop(1)
|
ls.Pop(1)
|
||||||
logger.Debug().Msgf("Lua function returned %q (%v)", lval, lval.Type().String())
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result := true
|
lval := ls.Get(-1)
|
||||||
if lua.LVIsFalse(lval) {
|
ls.Pop(1)
|
||||||
result = false
|
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 {
|
func (h *Host) handleBeforeMessageStored(msg event.InboundMessage) *event.InboundMessage {
|
||||||
@@ -193,10 +223,11 @@ func (h *Host) handleBeforeMessageStored(msg event.InboundMessage) *event.Inboun
|
|||||||
return nil
|
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())
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +235,6 @@ func (h *Host) handleBeforeMessageStored(msg event.InboundMessage) *event.Inboun
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Err(err).Msg("Bad response from Lua Function")
|
logger.Error().Err(err).Msg("Bad response from Lua Function")
|
||||||
}
|
}
|
||||||
ls.Pop(1)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,54 +9,12 @@ import (
|
|||||||
"github.com/inbucket/inbucket/v3/pkg/extension"
|
"github.com/inbucket/inbucket/v3/pkg/extension"
|
||||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||||
"github.com/inbucket/inbucket/v3/pkg/extension/luahost"
|
"github.com/inbucket/inbucket/v3/pkg/extension/luahost"
|
||||||
|
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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())
|
var consoleLogger = zerolog.New(zerolog.NewConsoleWriter())
|
||||||
|
|
||||||
func TestEmptyScript(t *testing.T) {
|
func TestEmptyScript(t *testing.T) {
|
||||||
@@ -92,11 +50,12 @@ func TestAfterMessageDeleted(t *testing.T) {
|
|||||||
-- Full message bindings tested elsewhere.
|
-- Full message bindings tested elsewhere.
|
||||||
assert_eq(msg.mailbox, "mb1")
|
assert_eq(msg.mailbox, "mb1")
|
||||||
assert_eq(msg.id, "id1")
|
assert_eq(msg.id, "id1")
|
||||||
notify:send(test_ok)
|
notify:send(asserts_ok)
|
||||||
end
|
end
|
||||||
`
|
`
|
||||||
extHost := extension.NewHost()
|
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)
|
require.NoError(t, err)
|
||||||
notify := luaHost.CreateChannel("notify")
|
notify := luaHost.CreateChannel("notify")
|
||||||
|
|
||||||
@@ -111,7 +70,7 @@ func TestAfterMessageDeleted(t *testing.T) {
|
|||||||
Size: 42,
|
Size: 42,
|
||||||
}
|
}
|
||||||
extHost.Events.AfterMessageDeleted.Emit(msg)
|
extHost.Events.AfterMessageDeleted.Emit(msg)
|
||||||
assertNotified(t, notify)
|
test.AssertNotified(t, notify)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAfterMessageStored(t *testing.T) {
|
func TestAfterMessageStored(t *testing.T) {
|
||||||
@@ -123,11 +82,12 @@ func TestAfterMessageStored(t *testing.T) {
|
|||||||
-- Full message bindings tested elsewhere.
|
-- Full message bindings tested elsewhere.
|
||||||
assert_eq(msg.mailbox, "mb1")
|
assert_eq(msg.mailbox, "mb1")
|
||||||
assert_eq(msg.id, "id1")
|
assert_eq(msg.id, "id1")
|
||||||
notify:send(test_ok)
|
notify:send(asserts_ok)
|
||||||
end
|
end
|
||||||
`
|
`
|
||||||
extHost := extension.NewHost()
|
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)
|
require.NoError(t, err)
|
||||||
notify := luaHost.CreateChannel("notify")
|
notify := luaHost.CreateChannel("notify")
|
||||||
|
|
||||||
@@ -142,36 +102,51 @@ func TestAfterMessageStored(t *testing.T) {
|
|||||||
Size: 42,
|
Size: 42,
|
||||||
}
|
}
|
||||||
extHost.Events.AfterMessageStored.Emit(msg)
|
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.
|
// Register lua event listener.
|
||||||
script := `
|
script := `
|
||||||
function inbucket.before.mail_accepted(localpart, domain)
|
function inbucket.before.mail_from_accepted(session)
|
||||||
return localpart == "from" and domain == "test"
|
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
|
end
|
||||||
`
|
`
|
||||||
extHost := extension.NewHost()
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Send event to be accepted.
|
{
|
||||||
addr := &event.AddressParts{Local: "from", Domain: "test"}
|
// Send event to be accepted.
|
||||||
got := extHost.Events.BeforeMailAccepted.Emit(addr)
|
session := event.SMTPSession{
|
||||||
want := true
|
From: &mail.Address{Name: "", Address: "from@example.com"},
|
||||||
require.NotNil(t, got, "Expected result from Emit()")
|
}
|
||||||
if *got != want {
|
got := extHost.Events.BeforeMailFromAccepted.Emit(&session)
|
||||||
t.Errorf("Got %v, wanted %v for addr %v", *got, want, addr)
|
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"}
|
// Send event to be denied.
|
||||||
got = extHost.Events.BeforeMailAccepted.Emit(addr)
|
session := event.SMTPSession{
|
||||||
want = false
|
From: &mail.Address{Name: "", Address: "from@reject.com"},
|
||||||
require.NotNil(t, got, "Expected result from Emit()")
|
}
|
||||||
if *got != want {
|
got := extHost.Events.BeforeMailFromAccepted.Emit(&session)
|
||||||
t.Errorf("Got %v, wanted %v for addr %v", *got, want, addr)
|
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.mailboxes, {"one", "two"})
|
||||||
assert_eq(msg.from.name, "From Name")
|
assert_eq(msg.from.name, "From Name")
|
||||||
assert_eq(msg.from.address, "from@example.com")
|
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].name, "To1 Name")
|
||||||
assert_eq(msg.to[1].address, "to1@example.com")
|
assert_eq(msg.to[1].address, "to1@example.com")
|
||||||
assert_eq(msg.to[2].name, "To2 Name")
|
assert_eq(msg.to[2].name, "To2 Name")
|
||||||
assert_eq(msg.to[2].address, "to2@example.com")
|
assert_eq(msg.to[2].address, "to2@example.com")
|
||||||
assert_eq(msg.subject, "inbound subj")
|
assert_eq(msg.subject, "inbound subj")
|
||||||
assert_eq(msg.size, 42)
|
assert_eq(msg.size, 42, "msg.size")
|
||||||
notify:send(test_ok)
|
notify:send(asserts_ok)
|
||||||
|
|
||||||
-- Generate response.
|
-- Generate response.
|
||||||
res = inbound_message.new()
|
res = inbound_message.new()
|
||||||
@@ -219,7 +194,8 @@ func TestBeforeMessageStored(t *testing.T) {
|
|||||||
end
|
end
|
||||||
`
|
`
|
||||||
extHost := extension.NewHost()
|
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)
|
require.NoError(t, err)
|
||||||
notify := luaHost.CreateChannel("notify")
|
notify := luaHost.CreateChannel("notify")
|
||||||
|
|
||||||
@@ -228,7 +204,7 @@ func TestBeforeMessageStored(t *testing.T) {
|
|||||||
require.NotNil(t, got, "Expected result from Emit()")
|
require.NotNil(t, got, "Expected result from Emit()")
|
||||||
|
|
||||||
// Verify Lua assertions passed.
|
// Verify Lua assertions passed.
|
||||||
assertNotified(t, notify)
|
test.AssertNotified(t, notify)
|
||||||
|
|
||||||
// Verify response values.
|
// Verify response values.
|
||||||
want := &event.InboundMessage{
|
want := &event.InboundMessage{
|
||||||
@@ -244,15 +220,84 @@ func TestBeforeMessageStored(t *testing.T) {
|
|||||||
assert.Equal(t, want, got, "Response InboundMessage did not match")
|
assert.Equal(t, want, got, "Response InboundMessage did not match")
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertNotified(t *testing.T, notify chan lua.LValue) {
|
func TestBeforeMessageStoredNilReturn(t *testing.T) {
|
||||||
t.Helper()
|
// Event to send.
|
||||||
select {
|
msg := event.InboundMessage{
|
||||||
case reslv := <-notify:
|
Mailboxes: []string{"one", "two"},
|
||||||
// Lua function received event.
|
From: &mail.Address{Name: "From Name", Address: "from@example.com"},
|
||||||
if lua.LVIsFalse(reslv) {
|
To: []*mail.Address{
|
||||||
t.Error("Lua responsed with false, wanted true")
|
{Name: "To1 Name", Address: "to1@example.com"},
|
||||||
}
|
{Name: "To2 Name", Address: "to2@example.com"},
|
||||||
case <-time.After(2 * time.Second):
|
},
|
||||||
t.Fatal("Lua did not respond to event within timeout")
|
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)
|
registerInbucketTypes(ls)
|
||||||
registerMailAddressType(ls)
|
registerMailAddressType(ls)
|
||||||
registerMessageMetadataType(ls)
|
registerMessageMetadataType(ls)
|
||||||
registerPolicyType(ls)
|
registerSMTPResponseType(ls)
|
||||||
|
registerSMTPSessionType(ls)
|
||||||
|
|
||||||
// Run compiled script.
|
// Run compiled script.
|
||||||
ls.Push(ls.NewFunctionFromProto(lp.funcProto))
|
ls.Push(ls.NewFunctionFromProto(lp.funcProto))
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func TestPoolGrowsWithPuts(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
b, err := pool.getState()
|
b, err := pool.getState()
|
||||||
require.NoError(t, err)
|
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(a)
|
||||||
pool.putState(b)
|
pool.putState(b)
|
||||||
@@ -64,11 +64,11 @@ func TestPoolPutDiscardsClosed(t *testing.T) {
|
|||||||
|
|
||||||
a, err := pool.getState()
|
a, err := pool.getState()
|
||||||
require.NoError(t, err)
|
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()
|
a.Close()
|
||||||
pool.putState(a)
|
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) {
|
func TestPoolPutClearsStack(t *testing.T) {
|
||||||
@@ -76,7 +76,7 @@ func TestPoolPutClearsStack(t *testing.T) {
|
|||||||
|
|
||||||
ls, err := pool.getState()
|
ls, err := pool.getState()
|
||||||
require.NoError(t, err)
|
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.
|
// Setup stack.
|
||||||
ls.Push(lua.LNumber(4))
|
ls.Push(lua.LNumber(4))
|
||||||
@@ -85,7 +85,7 @@ func TestPoolPutClearsStack(t *testing.T) {
|
|||||||
|
|
||||||
// Return and verify stack cleared.
|
// Return and verify stack cleared.
|
||||||
pool.putState(ls)
|
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")
|
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/extension/event"
|
||||||
"github.com/inbucket/inbucket/v3/pkg/policy"
|
"github.com/inbucket/inbucket/v3/pkg/policy"
|
||||||
"github.com/inbucket/inbucket/v3/pkg/storage"
|
"github.com/inbucket/inbucket/v3/pkg/storage"
|
||||||
"github.com/jhillyerd/enmime"
|
"github.com/jhillyerd/enmime/v2"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,9 +79,9 @@ func (s *StoreManager) Deliver(
|
|||||||
tstamp := now.UTC().Format(recvdTimeFmt)
|
tstamp := now.UTC().Format(recvdTimeFmt)
|
||||||
|
|
||||||
// Process inbound message through extensions.
|
// Process inbound message through extensions.
|
||||||
mailboxes := make([]string, len(recipients))
|
mailboxes := make([]string, 0, len(recipients))
|
||||||
for i, recip := range recipients {
|
for _, recip := range recipients {
|
||||||
mailboxes[i] = recip.Mailbox
|
mailboxes = append(mailboxes, recip.Mailbox)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct InboundMessage event and process through extensions.
|
// Construct InboundMessage event and process through extensions.
|
||||||
@@ -110,7 +110,7 @@ func (s *StoreManager) Deliver(
|
|||||||
|
|
||||||
// Deliver to each mailbox.
|
// Deliver to each mailbox.
|
||||||
for _, mb := range inbound.Mailboxes {
|
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)
|
recvd := fmt.Sprintf("%s for <%s>; %s\r\n", recvdHeader, mb, tstamp)
|
||||||
|
|
||||||
// Deliver message.
|
// Deliver message.
|
||||||
@@ -122,6 +122,7 @@ func (s *StoreManager) Deliver(
|
|||||||
To: inbound.To,
|
To: inbound.To,
|
||||||
Date: now,
|
Date: now,
|
||||||
Subject: inbound.Subject,
|
Subject: inbound.Subject,
|
||||||
|
Size: inbound.Size,
|
||||||
},
|
},
|
||||||
Reader: io.MultiReader(strings.NewReader(recvd), bytes.NewReader(source)),
|
Reader: io.MultiReader(strings.NewReader(recvd), bytes.NewReader(source)),
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-25
@@ -312,7 +312,7 @@ func TestDeliverEmitsAfterMessageStoredEvent(t *testing.T) {
|
|||||||
origin,
|
origin,
|
||||||
[]*policy.Recipient{recip},
|
[]*policy.Recipient{recip},
|
||||||
"Received: xyz\n",
|
"Received: xyz\n",
|
||||||
[]byte("From: from@example.com\n\ntest email"),
|
[]byte("From: from@example.com\nSubject: events\n\ntest email."),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -321,6 +321,17 @@ func TestDeliverEmitsAfterMessageStoredEvent(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotNil(t, got, "No event received, or it was nil")
|
assert.NotNil(t, got, "No event received, or it was nil")
|
||||||
assertMessageCount(t, sm, "to@example.com", 1)
|
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) {
|
func TestDeliverBeforeAndAfterMessageStoredEvents(t *testing.T) {
|
||||||
@@ -352,7 +363,7 @@ func TestDeliverBeforeAndAfterMessageStoredEvents(t *testing.T) {
|
|||||||
t.Fatal(err)
|
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.
|
// not guaranteed.
|
||||||
got1, err := listener()
|
got1, err := listener()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -368,10 +379,10 @@ func TestGetMessage(t *testing.T) {
|
|||||||
|
|
||||||
// Add a test message.
|
// Add a test message.
|
||||||
subject := "getMessage1"
|
subject := "getMessage1"
|
||||||
id := addTestMessage(sm, "box1", subject)
|
id := addTestMessage(sm, "get-box", subject)
|
||||||
|
|
||||||
// Verify retrieval of the test message.
|
// 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.NoError(t, err, "GetMessage must succeed")
|
||||||
require.NotNil(t, msg, "GetMessage must return a result")
|
require.NotNil(t, msg, "GetMessage must return a result")
|
||||||
assert.Equal(t, subject, msg.Subject)
|
assert.Equal(t, subject, msg.Subject)
|
||||||
@@ -383,19 +394,19 @@ func TestMarkSeen(t *testing.T) {
|
|||||||
|
|
||||||
// Add a test message.
|
// Add a test message.
|
||||||
subject := "getMessage1"
|
subject := "getMessage1"
|
||||||
id := addTestMessage(sm, "box1", subject)
|
id := addTestMessage(sm, "seen-box", subject)
|
||||||
|
|
||||||
// Verify test message unseen.
|
// 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.NoError(t, err, "GetMessage must succeed")
|
||||||
require.NotNil(t, msg, "GetMessage must return a result")
|
require.NotNil(t, msg, "GetMessage must return a result")
|
||||||
assert.False(t, msg.Seen, "msg should be unseen")
|
assert.False(t, msg.Seen, "msg should be unseen")
|
||||||
|
|
||||||
err = sm.MarkSeen("box1", id)
|
err = sm.MarkSeen("seen-box", id)
|
||||||
assert.NoError(t, err, "MarkSeen should succeed")
|
require.NoError(t, err, "MarkSeen should succeed")
|
||||||
|
|
||||||
// Verify test message seen.
|
// 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.NoError(t, err, "GetMessage must succeed")
|
||||||
require.NotNil(t, msg, "GetMessage must return a result")
|
require.NotNil(t, msg, "GetMessage must return a result")
|
||||||
assert.True(t, msg.Seen, "msg should have been seen")
|
assert.True(t, msg.Seen, "msg should have been seen")
|
||||||
@@ -405,17 +416,17 @@ func TestRemoveMessage(t *testing.T) {
|
|||||||
sm, _ := testStoreManager()
|
sm, _ := testStoreManager()
|
||||||
|
|
||||||
// Add test messages.
|
// Add test messages.
|
||||||
id1 := addTestMessage(sm, "box1", "subject 1")
|
id1 := addTestMessage(sm, "rm-box", "subject 1")
|
||||||
id2 := addTestMessage(sm, "box1", "subject 2")
|
id2 := addTestMessage(sm, "rm-box", "subject 2")
|
||||||
id3 := addTestMessage(sm, "box1", "subject 3")
|
id3 := addTestMessage(sm, "rm-box", "subject 3")
|
||||||
got, err := sm.GetMetadata("box1")
|
got, err := sm.GetMetadata("rm-box")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, got, 3)
|
require.Len(t, got, 3)
|
||||||
|
|
||||||
// Delete message 2 and verify.
|
// Delete message 2 and verify.
|
||||||
err = sm.RemoveMessage("box1", id2)
|
err = sm.RemoveMessage("rm-box", id2)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
got, err = sm.GetMetadata("box1")
|
got, err = sm.GetMetadata("rm-box")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, got, 2, "Should be 2 messages remaining")
|
require.Len(t, got, 2, "Should be 2 messages remaining")
|
||||||
|
|
||||||
@@ -431,19 +442,19 @@ func TestPurgeMessages(t *testing.T) {
|
|||||||
sm, _ := testStoreManager()
|
sm, _ := testStoreManager()
|
||||||
|
|
||||||
// Add test messages.
|
// Add test messages.
|
||||||
_ = addTestMessage(sm, "box1", "subject 1")
|
_ = addTestMessage(sm, "purge-box", "subject 1")
|
||||||
_ = addTestMessage(sm, "box1", "subject 2")
|
_ = addTestMessage(sm, "purge-box", "subject 2")
|
||||||
_ = addTestMessage(sm, "box1", "subject 3")
|
_ = addTestMessage(sm, "purge-box", "subject 3")
|
||||||
got, err := sm.GetMetadata("box1")
|
got, err := sm.GetMetadata("purge-box")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, got, 3)
|
require.Len(t, got, 3)
|
||||||
|
|
||||||
// Purge and verify.
|
// Purge and verify.
|
||||||
err = sm.PurgeMessages("box1")
|
err = sm.PurgeMessages("purge-box")
|
||||||
assert.NoError(t, err)
|
|
||||||
got, err = sm.GetMetadata("box1")
|
|
||||||
require.NoError(t, err)
|
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) {
|
func TestSourceReader(t *testing.T) {
|
||||||
@@ -543,7 +554,7 @@ func assertMessageCount(t *testing.T, sm *message.StoreManager, mailbox string,
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
metas, err := sm.GetMetadata(mailbox)
|
metas, err := sm.GetMetadata(mailbox)
|
||||||
assert.NoError(t, err, "StoreManager GetMetadata failed")
|
require.NoError(t, err, "StoreManager GetMetadata failed")
|
||||||
|
|
||||||
got := len(metas)
|
got := len(metas)
|
||||||
if got != count {
|
if got != count {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||||
"github.com/inbucket/inbucket/v3/pkg/storage"
|
"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.
|
// Message holds both the metadata and content of a message.
|
||||||
|
|||||||
+1
-1
@@ -140,7 +140,7 @@ func (hub *Hub) RemoveListener(l Listener) {
|
|||||||
// for unit tests.
|
// for unit tests.
|
||||||
func (hub *Hub) Sync() {
|
func (hub *Hub) Sync() {
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
hub.opChan <- func(h *Hub) {
|
hub.opChan <- func(_ *Hub) {
|
||||||
close(done)
|
close(done)
|
||||||
}
|
}
|
||||||
<-done
|
<-done
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package msghub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -51,7 +52,7 @@ func (l *testListener) Receive(msg event.MessageMetadata) error {
|
|||||||
close(l.overflow)
|
close(l.overflow)
|
||||||
}
|
}
|
||||||
if l.errorAfter > 0 && l.gotEvents > l.errorAfter {
|
if l.errorAfter > 0 && l.gotEvents > l.errorAfter {
|
||||||
return fmt.Errorf("Too many messages")
|
return errors.New("too many messages")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-24
@@ -2,6 +2,7 @@ package policy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
@@ -37,7 +38,7 @@ func (a *Addressing) ExtractMailbox(address string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if a.Config.MailboxNaming != config.FullNaming {
|
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 == "" {
|
if domain == "" {
|
||||||
@@ -45,7 +46,7 @@ func (a *Addressing) ExtractMailbox(address string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ValidateDomainPart(domain) {
|
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
|
return local + "@" + domain, nil
|
||||||
@@ -136,7 +137,7 @@ func ParseEmailAddress(address string) (local string, domain string, err error)
|
|||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
if !ValidateDomainPart(domain) {
|
if !ValidateDomainPart(domain) {
|
||||||
return "", "", fmt.Errorf("Domain part validation failed")
|
return "", "", errors.New("domain part validation failed")
|
||||||
}
|
}
|
||||||
return local, domain, nil
|
return local, domain, nil
|
||||||
}
|
}
|
||||||
@@ -229,7 +230,7 @@ func extractDomainMailbox(address string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ValidateDomainPart(domain) {
|
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
|
return domain, nil
|
||||||
@@ -240,26 +241,26 @@ func extractDomainMailbox(address string) (string, error) {
|
|||||||
// domain part is optional and not validated.
|
// domain part is optional and not validated.
|
||||||
func parseEmailAddress(address string) (local string, domain string, err error) {
|
func parseEmailAddress(address string) (local string, domain string, err error) {
|
||||||
if address == "" {
|
if address == "" {
|
||||||
return "", "", fmt.Errorf("empty address")
|
return "", "", errors.New("empty address")
|
||||||
}
|
}
|
||||||
if len(address) > 320 {
|
if len(address) > 320 {
|
||||||
return "", "", fmt.Errorf("address exceeds 320 characters")
|
return "", "", errors.New("address exceeds 320 characters")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove forward-path routes.
|
// Remove forward-path routes.
|
||||||
if address[0] == '@' {
|
if address[0] == '@' {
|
||||||
end := strings.IndexRune(address, ':')
|
end := strings.IndexRune(address, ':')
|
||||||
if end == -1 {
|
if end == -1 {
|
||||||
return "", "", fmt.Errorf("missing terminating ':' in route specification")
|
return "", "", errors.New("missing terminating ':' in route specification")
|
||||||
}
|
}
|
||||||
address = address[end+1:]
|
address = address[end+1:]
|
||||||
if address == "" {
|
if address == "" {
|
||||||
return "", "", fmt.Errorf("Address empty after removing route specification")
|
return "", "", errors.New("address empty after removing route specification")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if address[0] == '.' {
|
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.
|
// Loop over address parsing out local part.
|
||||||
@@ -285,7 +286,7 @@ LOOP:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
inCharQuote = false
|
inCharQuote = false
|
||||||
case bytes.IndexByte([]byte("!#$%&'*+-/=?^_`{|}~"), c) >= 0:
|
case strings.IndexByte("!#$%&'*+-/=?^_`{|}~", c) >= 0:
|
||||||
// These specials can be used unquoted.
|
// These specials can be used unquoted.
|
||||||
err = buf.WriteByte(c)
|
err = buf.WriteByte(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -296,7 +297,7 @@ LOOP:
|
|||||||
// A single period is OK.
|
// A single period is OK.
|
||||||
if prev == '.' {
|
if prev == '.' {
|
||||||
// Sequence of periods is not permitted.
|
// 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)
|
err = buf.WriteByte(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -306,19 +307,20 @@ LOOP:
|
|||||||
case c == '\\':
|
case c == '\\':
|
||||||
inCharQuote = true
|
inCharQuote = true
|
||||||
case c == '"':
|
case c == '"':
|
||||||
if inCharQuote {
|
switch {
|
||||||
|
case inCharQuote:
|
||||||
err = buf.WriteByte(c)
|
err = buf.WriteByte(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
inCharQuote = false
|
inCharQuote = false
|
||||||
} else if inStringQuote {
|
case inStringQuote:
|
||||||
inStringQuote = false
|
inStringQuote = false
|
||||||
} else {
|
default:
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
inStringQuote = true
|
inStringQuote = true
|
||||||
} else {
|
} 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 == '@':
|
case c == '@':
|
||||||
@@ -331,16 +333,16 @@ LOOP:
|
|||||||
} else {
|
} else {
|
||||||
// End of local-part.
|
// End of local-part.
|
||||||
if i > 128 {
|
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 == '.' {
|
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:]
|
domain = address[i+1:]
|
||||||
break LOOP
|
break LOOP
|
||||||
}
|
}
|
||||||
case c > 127:
|
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:
|
default:
|
||||||
if inCharQuote || inStringQuote {
|
if inCharQuote || inStringQuote {
|
||||||
err = buf.WriteByte(c)
|
err = buf.WriteByte(c)
|
||||||
@@ -349,16 +351,16 @@ LOOP:
|
|||||||
}
|
}
|
||||||
inCharQuote = false
|
inCharQuote = false
|
||||||
} else {
|
} else {
|
||||||
return "", "", fmt.Errorf("Character %q must be quoted", c)
|
return "", "", fmt.Errorf("character %q must be quoted", c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prev = c
|
prev = c
|
||||||
}
|
}
|
||||||
if inCharQuote {
|
if inCharQuote {
|
||||||
return "", "", fmt.Errorf("Cannot end address with unterminated quoted-pair")
|
return "", "", errors.New("cannot end address with unterminated quoted-pair")
|
||||||
}
|
}
|
||||||
if inStringQuote {
|
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
|
return buf.String(), domain, nil
|
||||||
}
|
}
|
||||||
@@ -369,7 +371,7 @@ LOOP:
|
|||||||
// quoted according to RFC3696.
|
// quoted according to RFC3696.
|
||||||
func parseMailboxName(localPart string) (result string, err error) {
|
func parseMailboxName(localPart string) (result string, err error) {
|
||||||
if localPart == "" {
|
if localPart == "" {
|
||||||
return "", fmt.Errorf("Mailbox name cannot be empty")
|
return "", errors.New("mailbox name cannot be empty")
|
||||||
}
|
}
|
||||||
result = strings.ToLower(localPart)
|
result = strings.ToLower(localPart)
|
||||||
invalid := make([]byte, 0, 10)
|
invalid := make([]byte, 0, 10)
|
||||||
@@ -378,13 +380,13 @@ func parseMailboxName(localPart string) (result string, err error) {
|
|||||||
switch {
|
switch {
|
||||||
case 'a' <= c && c <= 'z':
|
case 'a' <= c && c <= 'z':
|
||||||
case '0' <= c && c <= '9':
|
case '0' <= c && c <= '9':
|
||||||
case bytes.IndexByte([]byte("!#$%&'*+-=/?^_`.{|}~"), c) >= 0:
|
case strings.IndexByte("!#$%&'*+-=/?^_`.{|}~", c) >= 0:
|
||||||
default:
|
default:
|
||||||
invalid = append(invalid, c)
|
invalid = append(invalid, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(invalid) > 0 {
|
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 {
|
if idx := strings.Index(result, "+"); idx > -1 {
|
||||||
result = result[0:idx]
|
result = result[0:idx]
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ func TestShouldAcceptDomain(t *testing.T) {
|
|||||||
if got != tc.want {
|
if got != tc.want {
|
||||||
t.Errorf("Got %v for %q, want: %v", got, tc.domain, tc.want)
|
t.Errorf("Got %v for %q, want: %v", got, tc.domain, tc.want)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Test with default reject.
|
// Test with default reject.
|
||||||
@@ -60,7 +59,6 @@ func TestShouldAcceptDomain(t *testing.T) {
|
|||||||
if got != tc.want {
|
if got != tc.want {
|
||||||
t.Errorf("Got %v for %q, want: %v", got, tc.domain, 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 {
|
if got != tc.want {
|
||||||
t.Errorf("Got store %v for %q, want: %v", got, tc.domain, tc.want)
|
t.Errorf("Got store %v for %q, want: %v", got, tc.domain, tc.want)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Test with storage disabled.
|
// Test with storage disabled.
|
||||||
@@ -117,7 +114,6 @@ func TestShouldStoreDomain(t *testing.T) {
|
|||||||
if got != tc.want {
|
if got != tc.want {
|
||||||
t.Errorf("Got store %v for %q, want: %v", got, tc.domain, 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 {
|
for _, tc := range testTable {
|
||||||
if result, err := localPolicy.ExtractMailbox(tc.input); err != nil {
|
if result, err := localPolicy.ExtractMailbox(tc.input); err != nil {
|
||||||
t.Errorf("Error while parsing with local naming %q: %v", tc.input, err)
|
t.Errorf("Error while parsing with local naming %q: %v", tc.input, err)
|
||||||
} else {
|
} else if result != tc.local {
|
||||||
if result != tc.local {
|
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.local, result)
|
||||||
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.local, result)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if result, err := fullPolicy.ExtractMailbox(tc.input); err != nil {
|
if result, err := fullPolicy.ExtractMailbox(tc.input); err != nil {
|
||||||
t.Errorf("Error while parsing with full naming %q: %v", tc.input, err)
|
t.Errorf("Error while parsing with full naming %q: %v", tc.input, err)
|
||||||
} else {
|
} else if result != tc.full {
|
||||||
if result != tc.full {
|
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.full, result)
|
||||||
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.full, result)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if result, err := domainPolicy.ExtractMailbox(tc.input); tc.domain != "" && err != nil {
|
if result, err := domainPolicy.ExtractMailbox(tc.input); tc.domain != "" && err != nil {
|
||||||
t.Errorf("Error while parsing with domain naming %q: %v", tc.input, err)
|
t.Errorf("Error while parsing with domain naming %q: %v", tc.input, err)
|
||||||
} else {
|
} else if result != tc.domain {
|
||||||
if result != tc.domain {
|
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.domain, result)
|
||||||
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) {
|
t.Run(name, func(t *testing.T) {
|
||||||
if result, err := domainPolicy.ExtractMailbox(tc.input); tc.domain != "" && err != nil {
|
if result, err := domainPolicy.ExtractMailbox(tc.input); tc.domain != "" && err != nil {
|
||||||
t.Errorf("Error while parsing with domain naming %q: %v", tc.input, err)
|
t.Errorf("Error while parsing with domain naming %q: %v", tc.input, err)
|
||||||
} else {
|
} else if result != tc.domain {
|
||||||
if result != tc.domain {
|
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.domain, result)
|
||||||
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)
|
messages, err := ctx.Manager.GetMetadata(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// This doesn't indicate empty, likely an IO error
|
// 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))
|
jmessages := make([]*model.JSONMessageHeaderV1, len(messages))
|
||||||
for i, msg := range 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)
|
dec := json.NewDecoder(req.Body)
|
||||||
dm := model.JSONMessageHeaderV1{}
|
dm := model.JSONMessageHeaderV1{}
|
||||||
if err := dec.Decode(&dm); err != nil {
|
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 {
|
if dm.Seen {
|
||||||
err = ctx.Manager.MarkSeen(name, id)
|
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/extension/event"
|
||||||
"github.com/inbucket/inbucket/v3/pkg/message"
|
"github.com/inbucket/inbucket/v3/pkg/message"
|
||||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||||
"github.com/jhillyerd/enmime"
|
"github.com/jhillyerd/enmime/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRestMailboxList(t *testing.T) {
|
func TestRestMailboxList(t *testing.T) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -41,33 +42,56 @@ func New(baseURL string, opts ...Option) (*Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListMailbox returns a list of messages for the requested mailbox
|
// 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)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Client ref to each MessageHeader for convenience funcs.
|
||||||
for _, h := range headers {
|
for _, h := range headers {
|
||||||
h.client = c
|
h.client = c
|
||||||
}
|
}
|
||||||
return
|
|
||||||
|
return headers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMessage returns the message details given a mailbox name and message ID.
|
// GetMessage returns the message details given a mailbox name and message ID.
|
||||||
func (c *Client) GetMessage(name, id string) (message *Message, err error) {
|
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
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
message.client = c
|
message.client = c
|
||||||
return
|
return &message, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkSeen marks the specified message as having been read.
|
// MarkSeen marks the specified message as having been read.
|
||||||
func (c *Client) MarkSeen(name, id string) error {
|
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
|
uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id
|
||||||
err := c.doJSON("PATCH", uri, nil)
|
err := c.doJSON(ctx, "PATCH", uri, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.
|
// GetMessageSource returns the message source given a mailbox name and message ID.
|
||||||
func (c *Client) GetMessageSource(name, id string) (*bytes.Buffer, error) {
|
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"
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil,
|
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)
|
buf := new(bytes.Buffer)
|
||||||
_, err = buf.ReadFrom(resp.Body)
|
_, err = buf.ReadFrom(resp.Body)
|
||||||
return buf, err
|
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.
|
// DeleteMessage deletes a single message given the mailbox name and message ID.
|
||||||
func (c *Client) DeleteMessage(name, id string) error {
|
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
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PurgeMailbox deletes all messages in the given mailbox
|
// PurgeMailbox deletes all messages in the given mailbox
|
||||||
func (c *Client) PurgeMailbox(name string) error {
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,17 +174,32 @@ type MessageHeader struct {
|
|||||||
|
|
||||||
// GetMessage returns this message with content
|
// GetMessage returns this message with content
|
||||||
func (h *MessageHeader) GetMessage() (message *Message, err error) {
|
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
|
// GetSource returns the source for this message
|
||||||
func (h *MessageHeader) GetSource() (*bytes.Buffer, error) {
|
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
|
// Delete deletes this message from the mailbox
|
||||||
func (h *MessageHeader) Delete() error {
|
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
|
// Message represents an Inbucket message including content
|
||||||
@@ -151,10 +210,20 @@ type Message struct {
|
|||||||
|
|
||||||
// GetSource returns the source for this message
|
// GetSource returns the source for this message
|
||||||
func (m *Message) GetSource() (*bytes.Buffer, error) {
|
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
|
// Delete deletes this message from the mailbox
|
||||||
func (m *Message) Delete() error {
|
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.
|
// Option can apply itself to the private options type.
|
||||||
type Option interface {
|
type Option interface {
|
||||||
apply(*options)
|
apply(opts *options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDefaultOptions() *options {
|
func getDefaultOptions() *options {
|
||||||
|
|||||||
@@ -16,34 +16,42 @@ func Example() {
|
|||||||
baseURL, teardown := exampleSetup()
|
baseURL, teardown := exampleSetup()
|
||||||
defer teardown()
|
defer teardown()
|
||||||
|
|
||||||
// Begin by creating a new client using the base URL of your Inbucket server, i.e.
|
err := func() error {
|
||||||
// `localhost:9000`.
|
// Begin by creating a new client using the base URL of your Inbucket server, i.e.
|
||||||
restClient, err := client.New(baseURL)
|
// `localhost:9000`.
|
||||||
if err != nil {
|
restClient, err := client.New(baseURL)
|
||||||
log.Fatal(err)
|
if err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Get a slice of message headers for the mailbox named `user1`.
|
// Get a slice of message headers for the mailbox named `user1`.
|
||||||
headers, err := restClient.ListMailbox("user1")
|
headers, err := restClient.ListMailbox("user1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
for _, header := range headers {
|
for _, header := range headers {
|
||||||
fmt.Printf("ID: %v, Subject: %v\n", header.ID, header.Subject)
|
fmt.Printf("ID: %v, Subject: %v\n", header.ID, header.Subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the content of the first message.
|
// Get the content of the first message.
|
||||||
message, err := headers[0].GetMessage()
|
message, err := headers[0].GetMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("\nFrom: %v\n", message.From)
|
fmt.Printf("\nFrom: %v\n", message.From)
|
||||||
fmt.Printf("Text body:\n%v", message.Body.Text)
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Print(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output:
|
// Output:
|
||||||
|
|||||||
+9
-10
@@ -2,6 +2,7 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -11,7 +12,7 @@ import (
|
|||||||
|
|
||||||
// httpClient allows http.Client to be mocked for tests
|
// httpClient allows http.Client to be mocked for tests
|
||||||
type httpClient interface {
|
type httpClient interface {
|
||||||
Do(*http.Request) (*http.Response, error)
|
Do(req *http.Request) (*http.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic REST restClient
|
// Generic REST restClient
|
||||||
@@ -21,26 +22,24 @@ type restClient struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do performs an HTTP request with this client and returns the response.
|
// do performs an HTTP request with this client and returns the response.
|
||||||
func (c *restClient) do(method, uri string, body []byte) (*http.Response, error) {
|
func (c *restClient) do(ctx context.Context, method, uri string, body []byte) (*http.Response, error) {
|
||||||
rel, err := url.Parse(uri)
|
url := c.baseURL.JoinPath(uri)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
url := c.baseURL.ResolveReference(rel)
|
|
||||||
var r io.Reader
|
var r io.Reader
|
||||||
if body != nil {
|
if body != nil {
|
||||||
r = bytes.NewReader(body)
|
r = bytes.NewReader(body)
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest(method, url.String(), r)
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, url.String(), r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s for %q: %v", method, url, err)
|
return nil, fmt.Errorf("%s for %q: %v", method, url, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.client.Do(req)
|
return c.client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// doJSON performs an HTTP request with this client and marshalls the JSON response into v.
|
// 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 {
|
func (c *restClient) doJSON(ctx context.Context, method string, uri string, v interface{}) error {
|
||||||
resp, err := c.do(method, uri, nil)
|
resp, err := c.do(ctx, method, uri, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,33 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const baseURLStr = "http://test.local:8080"
|
const baseURLStr = "http://test.local:8080"
|
||||||
|
const baseURLPathStr = "http://test.local:8080/inbucket"
|
||||||
|
|
||||||
var baseURL *url.URL
|
var baseURL *url.URL
|
||||||
|
|
||||||
|
var baseURLPath *url.URL
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
var err error
|
var err error
|
||||||
baseURL, err = url.Parse(baseURLStr)
|
baseURL, err = url.Parse(baseURLStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
baseURLPath, err = url.Parse(baseURLPathStr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockHTTPClient struct {
|
type mockHTTPClient struct {
|
||||||
@@ -51,32 +62,42 @@ func (m *mockHTTPClient) ReqBody() []byte {
|
|||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDo(t *testing.T) {
|
func TestDoTable(t *testing.T) {
|
||||||
var want, got string
|
tests := []struct {
|
||||||
mth := &mockHTTPClient{}
|
method string
|
||||||
c := &restClient{mth, baseURL}
|
uri string
|
||||||
body := []byte("Test body")
|
wantMethod string
|
||||||
|
base *url.URL
|
||||||
_, err := c.do("POST", "/dopost", body)
|
wantURL string
|
||||||
if err != nil {
|
wantBody []byte
|
||||||
t.Fatal(err)
|
}{
|
||||||
|
{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"
|
resp, err := c.do(ctx, test.method, test.uri, test.wantBody)
|
||||||
got = mth.req.Method
|
require.NoError(t, err)
|
||||||
if got != want {
|
err = resp.Body.Close()
|
||||||
t.Errorf("req.Method == %q, want %q", got, want)
|
require.NoError(t, err)
|
||||||
}
|
|
||||||
|
|
||||||
want = baseURLStr + "/dopost"
|
if mth.req.Method != test.wantMethod {
|
||||||
got = mth.req.URL.String()
|
t.Errorf("req.Method == %q, want %q", mth.req.Method, test.wantMethod)
|
||||||
if got != want {
|
}
|
||||||
t.Errorf("req.URL == %q, want %q", got, want)
|
if mth.req.URL.String() != test.wantURL {
|
||||||
}
|
t.Errorf("req.URL == %q, want %q", mth.req.URL.String(), test.wantURL)
|
||||||
|
}
|
||||||
b := mth.ReqBody()
|
if !bytes.Equal(mth.ReqBody(), test.wantBody) {
|
||||||
if !bytes.Equal(b, body) {
|
t.Errorf("req.Body == %q, want %q", mth.ReqBody(), test.wantBody)
|
||||||
t.Errorf("req.Body == %q, want %q", b, body)
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +110,7 @@ func TestDoJSON(t *testing.T) {
|
|||||||
c := &restClient{mth, baseURL}
|
c := &restClient{mth, baseURL}
|
||||||
|
|
||||||
var v map[string]interface{}
|
var v map[string]interface{}
|
||||||
err := c.doJSON("GET", "/doget", &v)
|
err := c.doJSON(context.Background(), "GET", "/doget", &v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -123,7 +144,7 @@ func TestDoJSONNilV(t *testing.T) {
|
|||||||
mth := &mockHTTPClient{}
|
mth := &mockHTTPClient{}
|
||||||
c := &restClient{mth, baseURL}
|
c := &restClient{mth, baseURL}
|
||||||
|
|
||||||
err := c.doJSON("GET", "/doget", nil)
|
err := c.doJSON(context.Background(), "GET", "/doget", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -1,7 +1,9 @@
|
|||||||
package rest
|
package rest
|
||||||
|
|
||||||
import "github.com/gorilla/mux"
|
import (
|
||||||
import "github.com/inbucket/inbucket/v3/pkg/server/web"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/inbucket/inbucket/v3/pkg/server/web"
|
||||||
|
)
|
||||||
|
|
||||||
// SetupRoutes populates the routes for the REST interface
|
// SetupRoutes populates the routes for the REST interface
|
||||||
func SetupRoutes(r *mux.Router) {
|
func SetupRoutes(r *mux.Router) {
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package rest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/inbucket/inbucket/v3/pkg/config"
|
"github.com/inbucket/inbucket/v3/pkg/config"
|
||||||
"github.com/inbucket/inbucket/v3/pkg/message"
|
"github.com/inbucket/inbucket/v3/pkg/message"
|
||||||
@@ -16,24 +18,36 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func testRestGet(url string) (*httptest.ResponseRecorder, error) {
|
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")
|
req.Header.Add("Accept", "application/json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass request to handlers directly.
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
web.Router.ServeHTTP(w, req)
|
web.Router.ServeHTTP(w, req)
|
||||||
|
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRestPatch(url string, body string) (*httptest.ResponseRecorder, error) {
|
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")
|
req.Header.Add("Accept", "application/json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass request to handlers directly.
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
web.Router.ServeHTTP(w, req)
|
web.Router.ServeHTTP(w, req)
|
||||||
|
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,9 +136,10 @@ func getDecodedPath(o interface{}, path ...string) (interface{}, string) {
|
|||||||
if o == nil {
|
if o == nil {
|
||||||
return nil, " is nil"
|
return nil, " is nil"
|
||||||
}
|
}
|
||||||
key := path[0]
|
|
||||||
present := false
|
var present bool
|
||||||
var val interface{}
|
var val interface{}
|
||||||
|
key := path[0]
|
||||||
if key[0] == '[' {
|
if key[0] == '[' {
|
||||||
// Expecting slice.
|
// Expecting slice.
|
||||||
index, err := strconv.Atoi(strings.Trim(key, "[]"))
|
index, err := strconv.Atoi(strings.Trim(key, "[]"))
|
||||||
@@ -147,12 +162,15 @@ func getDecodedPath(o interface{}, path ...string) (interface{}, string) {
|
|||||||
}
|
}
|
||||||
val, present = omap[key]
|
val, present = omap[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
if !present {
|
if !present {
|
||||||
return nil, "/" + key + " is missing"
|
return nil, "/" + key + " is missing"
|
||||||
}
|
}
|
||||||
|
|
||||||
result, msg := getDecodedPath(val, path[1:]...)
|
result, msg := getDecodedPath(val, path[1:]...)
|
||||||
if msg != "" {
|
if msg != "" {
|
||||||
return nil, "/" + key + msg
|
return nil, "/" + key + msg
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, ""
|
return result, ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func FullAssembly(conf *config.Root) (*Services, error) {
|
|||||||
// Configure extensions.
|
// Configure extensions.
|
||||||
extHost := extension.NewHost()
|
extHost := extension.NewHost()
|
||||||
luaHost, err := luahost.New(conf.Lua, extHost)
|
luaHost, err := luahost.New(conf.Lua, extHost)
|
||||||
if err != nil {
|
if err != nil && err != luahost.ErrNoScript {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-46
@@ -134,48 +134,46 @@ func (s *Server) startSession(id int, conn net.Conn) {
|
|||||||
line, err := ssn.readLine()
|
line, err := ssn.readLine()
|
||||||
ssn.logger.Debug().Msgf("read %s", line)
|
ssn.logger.Debug().Msgf("read %s", line)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if cmd, arg, ok := ssn.parseCmd(line); ok {
|
cmd, arg := ssn.parseCmd(line)
|
||||||
// Check against valid SMTP commands
|
// Commands we handle in any state
|
||||||
if cmd == "" {
|
if cmd == "CAPA" {
|
||||||
ssn.send("-ERR Speak up")
|
// List our capabilities per RFC2449
|
||||||
continue
|
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(".")
|
||||||
ssn.send(fmt.Sprintf("-ERR Syntax error, %v command unrecognized", cmd))
|
continue
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(cmd, arg)
|
||||||
|
continue
|
||||||
|
case TRANSACTION:
|
||||||
|
ssn.transactionHandler(cmd, arg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ssn.logger.Error().Msgf("Session entered unexpected state %v", ssn.state)
|
||||||
|
break
|
||||||
} else {
|
} else {
|
||||||
// readLine() returned an error
|
// readLine() returned an error
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
@@ -220,13 +218,13 @@ func (s *Session) authorizationHandler(cmd string, args []string) {
|
|||||||
// Invalid command since TLS unconfigured.
|
// Invalid command since TLS unconfigured.
|
||||||
s.logger.Debug().Msgf("-ERR TLS unavailable on the server")
|
s.logger.Debug().Msgf("-ERR TLS unavailable on the server")
|
||||||
s.send("-ERR TLS unavailable on the server")
|
s.send("-ERR TLS unavailable on the server")
|
||||||
s.ooSeq(cmd)
|
return
|
||||||
}
|
}
|
||||||
if s.tlsState != nil {
|
if s.tlsState != nil {
|
||||||
// TLS state previously valid.
|
// TLS state previously valid.
|
||||||
s.logger.Debug().Msg("-ERR A TLS session already agreed upon.")
|
s.logger.Debug().Msg("-ERR A TLS session already agreed upon.")
|
||||||
s.send("-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.")
|
s.logger.Debug().Msg("Initiating TLS context.")
|
||||||
|
|
||||||
@@ -463,7 +461,7 @@ func (s *Session) transactionHandler(cmd string, args []string) {
|
|||||||
s.processDeletes()
|
s.processDeletes()
|
||||||
s.enterState(QUIT)
|
s.enterState(QUIT)
|
||||||
case "NOOP":
|
case "NOOP":
|
||||||
s.send("+OK I have sucessfully done nothing")
|
s.send("+OK I have successfully done nothing")
|
||||||
case "RSET":
|
case "RSET":
|
||||||
// Reset session, don't actually delete anything I told you to
|
// Reset session, don't actually delete anything I told you to
|
||||||
s.logger.Debug().Msgf("Resetting session state on RSET request")
|
s.logger.Debug().Msgf("Resetting session state on RSET request")
|
||||||
@@ -631,14 +629,14 @@ func (s *Session) readLine() (line string, err error) {
|
|||||||
return line, nil
|
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")
|
line = strings.TrimRight(line, "\r\n")
|
||||||
if line == "" {
|
if line == "" {
|
||||||
return "", nil, true
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
words := strings.Split(line, " ")
|
words := strings.Split(line, " ")
|
||||||
return strings.ToUpper(words[0]), words[1:], true
|
return strings.ToUpper(words[0]), words[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) reset() {
|
func (s *Session) reset() {
|
||||||
|
|||||||
+136
-50
@@ -29,8 +29,7 @@ func TestNoTLS(t *testing.T) {
|
|||||||
pipe := setupPOPSession(t, server)
|
pipe := setupPOPSession(t, server)
|
||||||
c := textproto.NewConn(pipe)
|
c := textproto.NewConn(pipe)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = c.PrintfLine("QUIT")
|
_ = c.Close()
|
||||||
_, _ = c.ReadLine()
|
|
||||||
server.Drain()
|
server.Drain()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -41,21 +40,15 @@ func TestNoTLS(t *testing.T) {
|
|||||||
if !strings.HasPrefix(reply, "+OK") {
|
if !strings.HasPrefix(reply, "+OK") {
|
||||||
t.Fatalf("Initial line is not +OK")
|
t.Fatalf("Initial line is not +OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify CAPA response does not include STLS.
|
||||||
if err := c.PrintfLine("CAPA"); err != nil {
|
if err := c.PrintfLine("CAPA"); err != nil {
|
||||||
t.Fatalf("Failed to send CAPA; %v.", err)
|
t.Fatalf("Failed to send CAPA; %v.", err)
|
||||||
}
|
}
|
||||||
replies := []string{}
|
replies, err := c.ReadDotLines()
|
||||||
for {
|
if err != nil {
|
||||||
reply, err := c.ReadLine()
|
t.Fatalf("Reading CAPA line failed %v", err)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Reading CAPA line failed %v", err)
|
|
||||||
}
|
|
||||||
if reply == "." {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
replies = append(replies, reply)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range replies {
|
for _, r := range replies {
|
||||||
if r == "STLS" {
|
if r == "STLS" {
|
||||||
t.Errorf("TLS not enabled but received 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) {
|
func TestStartTLS(t *testing.T) {
|
||||||
ds := test.NewStore()
|
ds := test.NewStore()
|
||||||
server := setupPOPServer(t, ds, true, false)
|
server := setupPOPServer(t, ds, true, false)
|
||||||
pipe := setupPOPSession(t, server)
|
pipe := setupPOPSession(t, server)
|
||||||
c := textproto.NewConn(pipe)
|
c := textproto.NewConn(pipe)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = c.PrintfLine("QUIT")
|
_ = c.Close()
|
||||||
_, _ = c.ReadLine()
|
|
||||||
server.Drain()
|
server.Drain()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -81,28 +104,21 @@ func TestStartTLS(t *testing.T) {
|
|||||||
if !strings.HasPrefix(reply, "+OK") {
|
if !strings.HasPrefix(reply, "+OK") {
|
||||||
t.Fatalf("Initial line is not +OK")
|
t.Fatalf("Initial line is not +OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify CAPA response does not include STLS.
|
||||||
if err := c.PrintfLine("CAPA"); err != nil {
|
if err := c.PrintfLine("CAPA"); err != nil {
|
||||||
t.Fatalf("Failed to send CAPA; %v.", err)
|
t.Fatalf("Failed to send CAPA; %v.", err)
|
||||||
}
|
}
|
||||||
replies := []string{}
|
replies, err := c.ReadDotLines()
|
||||||
for {
|
if err != nil {
|
||||||
reply, err := c.ReadLine()
|
t.Fatalf("Reading CAPA line failed %v", err)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Reading CAPA line failed %v", err)
|
|
||||||
}
|
|
||||||
if reply == "." {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
replies = append(replies, reply)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sawTLS := false
|
sawTLS := false
|
||||||
for _, r := range replies {
|
for _, r := range replies {
|
||||||
if r == "STLS" {
|
if r == "STLS" {
|
||||||
sawTLS = true
|
sawTLS = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sawTLS {
|
if !sawTLS {
|
||||||
t.Errorf("TLS enabled but no STLS capability.")
|
t.Errorf("TLS enabled but no STLS capability.")
|
||||||
}
|
}
|
||||||
@@ -138,15 +154,95 @@ func TestStartTLS(t *testing.T) {
|
|||||||
if !strings.HasPrefix(reply, "+OK") {
|
if !strings.HasPrefix(reply, "+OK") {
|
||||||
t.Fatalf("CAPA failed: %s", reply)
|
t.Fatalf("CAPA failed: %s", reply)
|
||||||
}
|
}
|
||||||
for {
|
_, err = c.ReadDotLines()
|
||||||
reply, err := c.ReadLine()
|
if err != nil {
|
||||||
if err != nil {
|
t.Fatalf("Reading CAPA line failed %v", err)
|
||||||
t.Fatalf("Reading CAPA line failed %v", err)
|
}
|
||||||
}
|
}
|
||||||
if reply == "." {
|
|
||||||
break
|
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) {
|
func TestForceTLS(t *testing.T) {
|
||||||
@@ -165,8 +261,7 @@ func TestForceTLS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
c := textproto.NewConn(tlsConn)
|
c := textproto.NewConn(tlsConn)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = c.PrintfLine("QUIT")
|
_ = c.Close()
|
||||||
_, _ = c.ReadLine()
|
|
||||||
server.Drain()
|
server.Drain()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -178,27 +273,18 @@ func TestForceTLS(t *testing.T) {
|
|||||||
t.Fatalf("Initial line is not +OK")
|
t.Fatalf("Initial line is not +OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify CAPA response does not include STLS.
|
||||||
if err := c.PrintfLine("CAPA"); err != nil {
|
if err := c.PrintfLine("CAPA"); err != nil {
|
||||||
t.Fatalf("Failed to send CAPA; %v.", err)
|
t.Fatalf("Failed to send CAPA; %v.", err)
|
||||||
}
|
}
|
||||||
reply, err = c.ReadLine()
|
replies, err := c.ReadDotLines()
|
||||||
if err != nil {
|
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") {
|
for _, r := range replies {
|
||||||
t.Fatalf("CAPA failed: %s", reply)
|
if r == "STLS" {
|
||||||
}
|
|
||||||
for {
|
|
||||||
reply, err := c.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Reading CAPA line failed %v", err)
|
|
||||||
}
|
|
||||||
if reply == "STLS" {
|
|
||||||
t.Errorf("STLS in CAPA in forceTLS mode.")
|
t.Errorf("STLS in CAPA in forceTLS mode.")
|
||||||
}
|
}
|
||||||
if reply == "." {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func NewServer(pop3Config config.POP3, store storage.Store) (*Server, error) {
|
|||||||
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(pop3Config.TLSCert, pop3Config.TLSPrivKey)
|
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(pop3Config.TLSCert, pop3Config.TLSPrivKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error().Msgf("Failed loading X509 KeyPair: %v", err)
|
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.
|
// Do not silently turn off Security.
|
||||||
}
|
}
|
||||||
slog.Debug().Msg("TLS config available")
|
slog.Debug().Msg("TLS config available")
|
||||||
@@ -97,8 +97,8 @@ func (s *Server) serve(ctx context.Context) {
|
|||||||
} else {
|
} else {
|
||||||
tempDelay *= 2
|
tempDelay *= 2
|
||||||
}
|
}
|
||||||
if max := 1 * time.Second; tempDelay > max {
|
if maxDelay := 1 * time.Second; tempDelay > maxDelay {
|
||||||
tempDelay = max
|
tempDelay = maxDelay
|
||||||
}
|
}
|
||||||
log.Error().Str("module", "pop3").Err(err).
|
log.Error().Str("module", "pop3").Err(err).
|
||||||
Msgf("POP3 accept timout; retrying in %v", tempDelay)
|
Msgf("POP3 accept timout; retrying in %v", tempDelay)
|
||||||
|
|||||||
+177
-108
@@ -4,9 +4,11 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/mail"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -114,7 +116,11 @@ type Session struct {
|
|||||||
// NewSession creates a new Session for the given connection
|
// NewSession creates a new Session for the given connection
|
||||||
func NewSession(server *Server, id int, conn net.Conn, logger zerolog.Logger) *Session {
|
func NewSession(server *Server, id int, conn net.Conn, logger zerolog.Logger) *Session {
|
||||||
reader := bufio.NewReader(conn)
|
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{
|
session := &Session{
|
||||||
Server: server,
|
Server: server,
|
||||||
@@ -122,7 +128,7 @@ func NewSession(server *Server, id int, conn net.Conn, logger zerolog.Logger) *S
|
|||||||
conn: conn,
|
conn: conn,
|
||||||
state: GREET,
|
state: GREET,
|
||||||
reader: reader,
|
reader: reader,
|
||||||
remoteHost: host,
|
remoteHost: remoteHost,
|
||||||
recipients: make([]*policy.Recipient, 0),
|
recipients: make([]*policy.Recipient, 0),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
debug: server.config.Debug,
|
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)
|
return fmt.Sprintf("Session{id: %v, state: %v}", s.id, s.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Session flow:
|
// Session flow:
|
||||||
* 1. Send initial greeting
|
// 1. Send initial greeting
|
||||||
* 2. Receive cmd
|
// 2. Receive cmd
|
||||||
* 3. If good cmd, respond, optionally change state
|
// 3. If good cmd, respond, optionally change state
|
||||||
* 4. If bad cmd, respond error
|
// 4. If bad cmd, respond error
|
||||||
* 5. Goto 2
|
// 5. Goto 2
|
||||||
*/
|
|
||||||
func (s *Server) startSession(id int, conn net.Conn, logger zerolog.Logger) {
|
func (s *Server) startSession(id int, conn net.Conn, logger zerolog.Logger) {
|
||||||
logger = logger.Hook(logHook{}).With().
|
logger = logger.Hook(logHook{}).With().
|
||||||
Str("module", "smtp").
|
Str("module", "smtp").
|
||||||
Str("remote", conn.RemoteAddr().String()).
|
Str("remote", conn.RemoteAddr().String()).
|
||||||
Int("session", id).Logger()
|
Int("session", id).Logger()
|
||||||
logger.Info().Msg("Starting SMTP session")
|
logger.Info().Msg("Starting SMTP session")
|
||||||
|
|
||||||
|
// Update WaitGroup and counters.
|
||||||
|
s.wg.Add(1)
|
||||||
expConnectsCurrent.Add(1)
|
expConnectsCurrent.Add(1)
|
||||||
|
expConnectsTotal.Add(1)
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := conn.Close(); err != nil {
|
if err := conn.Close(); err != nil {
|
||||||
logger.Warn().Err(err).Msg("Closing connection")
|
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()
|
line, err := ssn.readLine()
|
||||||
if err == nil {
|
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 {
|
switch ssn.state {
|
||||||
case LOGIN:
|
case LOGIN:
|
||||||
ssn.loginHandler(line)
|
ssn.loginHandler()
|
||||||
continue
|
continue
|
||||||
case PASSWORD:
|
case PASSWORD:
|
||||||
ssn.passwordHandler(line)
|
ssn.passwordHandler()
|
||||||
continue
|
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")
|
ssn.send("252 Cannot VRFY user, but will accept message")
|
||||||
continue
|
continue
|
||||||
case "NOOP":
|
case "NOOP":
|
||||||
ssn.send("250 I have sucessfully done nothing")
|
ssn.send("250 I have successfully done nothing")
|
||||||
continue
|
continue
|
||||||
case "RSET":
|
case "RSET":
|
||||||
// Reset session
|
// Reset session
|
||||||
@@ -307,18 +316,18 @@ func parseHelloArgument(arg string) (string, error) {
|
|||||||
domain = arg[:idx]
|
domain = arg[:idx]
|
||||||
}
|
}
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
return "", fmt.Errorf("Invalid domain")
|
return "", errors.New("invalid domain")
|
||||||
}
|
}
|
||||||
return domain, nil
|
return domain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) loginHandler(line string) {
|
func (s *Session) loginHandler() {
|
||||||
// Content and length of username is ignored.
|
// Content and length of username is ignored.
|
||||||
s.send(fmt.Sprintf("334 %v", passwordChallenge))
|
s.send(fmt.Sprintf("334 %v", passwordChallenge))
|
||||||
s.enterState(PASSWORD)
|
s.enterState(PASSWORD)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) passwordHandler(line string) {
|
func (s *Session) passwordHandler() {
|
||||||
// Content and length of password is ignored.
|
// Content and length of password is ignored.
|
||||||
s.send("235 Authentication successful")
|
s.send("235 Authentication successful")
|
||||||
s.enterState(READY)
|
s.enterState(READY)
|
||||||
@@ -327,7 +336,8 @@ func (s *Session) passwordHandler(line string) {
|
|||||||
// READY state -> waiting for MAIL
|
// READY state -> waiting for MAIL
|
||||||
// AUTH can change
|
// AUTH can change
|
||||||
func (s *Session) readyHandler(cmd string, arg string) {
|
func (s *Session) readyHandler(cmd string, arg string) {
|
||||||
if cmd == "STARTTLS" {
|
switch cmd {
|
||||||
|
case "STARTTLS":
|
||||||
if !s.Server.config.TLSEnabled {
|
if !s.Server.config.TLSEnabled {
|
||||||
// Invalid command since TLS unconfigured.
|
// Invalid command since TLS unconfigured.
|
||||||
s.logger.Debug().Msgf("454 TLS unavailable on the server")
|
s.logger.Debug().Msgf("454 TLS unavailable on the server")
|
||||||
@@ -350,116 +360,136 @@ func (s *Session) readyHandler(cmd string, arg string) {
|
|||||||
s.tlsState = new(tls.ConnectionState)
|
s.tlsState = new(tls.ConnectionState)
|
||||||
*s.tlsState = tlsConn.ConnectionState()
|
*s.tlsState = tlsConn.ConnectionState()
|
||||||
s.enterState(GREET)
|
s.enterState(GREET)
|
||||||
} else if cmd == "AUTH" {
|
|
||||||
|
case "AUTH":
|
||||||
args := strings.SplitN(arg, " ", 3)
|
args := strings.SplitN(arg, " ", 3)
|
||||||
authMethod := args[0]
|
authMethod := args[0]
|
||||||
switch authMethod {
|
switch authMethod {
|
||||||
case "PLAIN":
|
case "PLAIN":
|
||||||
{
|
if len(args) != 2 {
|
||||||
if len(args) != 2 {
|
s.send("500 Bad auth arguments")
|
||||||
s.send("500 Bad auth arguments")
|
s.logger.Warn().Msgf("Bad auth attempt: %q", arg)
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
s.logger.Info().Msgf("Accepting credentials: %q", args[1])
|
||||||
|
s.send("235 2.7.0 Authentication successful")
|
||||||
|
return
|
||||||
|
|
||||||
case "LOGIN":
|
case "LOGIN":
|
||||||
{
|
s.send(fmt.Sprintf("334 %v", usernameChallenge))
|
||||||
s.send(fmt.Sprintf("334 %v", usernameChallenge))
|
s.enterState(LOGIN)
|
||||||
s.enterState(LOGIN)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
{
|
s.send(fmt.Sprintf("500 Unsupported AUTH method: %v", authMethod))
|
||||||
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)
|
|
||||||
return
|
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 {
|
case "MAIL":
|
||||||
s.send("501 Bad sender address syntax")
|
s.parseMailFromCmd(arg)
|
||||||
s.logger.Warn().Msgf("Bad address as MAIL arg: %q, %s", from, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if from == "" {
|
|
||||||
from = "unspecified"
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is where the client may put BODY=8BITMIME, but we already
|
case "EHLO":
|
||||||
// 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" {
|
|
||||||
// Reset session
|
// Reset session
|
||||||
s.logger.Debug().Msgf("Resetting session state on EHLO request")
|
s.logger.Debug().Msgf("Resetting session state on EHLO request")
|
||||||
s.reset()
|
s.reset()
|
||||||
s.send("250 Session reset")
|
s.send("250 Session reset")
|
||||||
} else {
|
|
||||||
|
default:
|
||||||
s.ooSeq(cmd)
|
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 from address.
|
||||||
|
_, domain, err := policy.ParseEmailAddress(from)
|
||||||
|
s.logger.Debug().Msgf("Origin domain is %v", domain)
|
||||||
|
if from != "" && err != nil {
|
||||||
|
s.send("501 Bad sender address syntax")
|
||||||
|
s.logger.Warn().Msgf("Bad address as MAIL arg: %q, %s", from, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if from == "" {
|
||||||
|
from = "unspecified"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse ESMTP parameters.
|
||||||
|
if m[2] != "" {
|
||||||
|
// Here the client may put BODY=8BITMIME, but Inbucket already
|
||||||
|
// 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", 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
|
// MAIL state -> waiting for RCPTs followed by DATA
|
||||||
func (s *Session) mailHandler(cmd string, arg string) {
|
func (s *Session) mailHandler(cmd string, arg string) {
|
||||||
switch cmd {
|
switch cmd {
|
||||||
@@ -476,7 +506,26 @@ func (s *Session) mailHandler(cmd string, arg string) {
|
|||||||
s.logger.Warn().Str("to", addr).Err(err).Msg("Bad address as RCPT arg")
|
s.logger.Warn().Str("to", addr).Err(err).Msg("Bad address as RCPT arg")
|
||||||
return
|
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.logger.Warn().Str("to", addr).Msg("Rejecting recipient domain")
|
||||||
s.send("550 Relay not permitted")
|
s.send("550 Relay not permitted")
|
||||||
return
|
return
|
||||||
@@ -668,3 +717,23 @@ func (s *Session) ooSeq(cmd string) {
|
|||||||
s.send(fmt.Sprintf("503 Command %v is out of sequence", cmd))
|
s.send(fmt.Sprintf("503 Command %v is out of sequence", cmd))
|
||||||
s.logger.Warn().Msgf("Wasn't expecting %v here", 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+295
-172
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type scriptStep struct {
|
type scriptStep struct {
|
||||||
@@ -45,13 +46,10 @@ func TestGreetStateValidCommands(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.send, func(t *testing.T) {
|
t.Run(tc.send, func(t *testing.T) {
|
||||||
defer server.Drain() // Required to prevent test logging data race.
|
|
||||||
script := []scriptStep{
|
script := []scriptStep{
|
||||||
tc,
|
tc,
|
||||||
{"QUIT", 221}}
|
{"QUIT", 221}}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +58,6 @@ func TestGreetStateValidCommands(t *testing.T) {
|
|||||||
func TestGreetState(t *testing.T) {
|
func TestGreetState(t *testing.T) {
|
||||||
ds := test.NewStore()
|
ds := test.NewStore()
|
||||||
server := setupSMTPServer(ds, extension.NewHost())
|
server := setupSMTPServer(ds, extension.NewHost())
|
||||||
defer server.Drain() // Required to prevent test logging data race.
|
|
||||||
|
|
||||||
tests := []scriptStep{
|
tests := []scriptStep{
|
||||||
{"HELO", 501},
|
{"HELO", 501},
|
||||||
@@ -73,13 +70,10 @@ func TestGreetState(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.send, func(t *testing.T) {
|
t.Run(tc.send, func(t *testing.T) {
|
||||||
defer server.Drain() // Required to prevent test logging data race.
|
|
||||||
script := []scriptStep{
|
script := []scriptStep{
|
||||||
tc,
|
tc,
|
||||||
{"QUIT", 221}}
|
{"QUIT", 221}}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,32 +81,26 @@ func TestGreetState(t *testing.T) {
|
|||||||
func TestEmptyEnvelope(t *testing.T) {
|
func TestEmptyEnvelope(t *testing.T) {
|
||||||
ds := test.NewStore()
|
ds := test.NewStore()
|
||||||
server := setupSMTPServer(ds, extension.NewHost())
|
server := setupSMTPServer(ds, extension.NewHost())
|
||||||
defer server.Drain()
|
|
||||||
|
|
||||||
// Test out some empty envelope without blanks
|
// Test out some empty envelope without blanks
|
||||||
script := []scriptStep{
|
script := []scriptStep{
|
||||||
{"HELO localhost", 250},
|
{"HELO localhost", 250},
|
||||||
{"MAIL FROM:<>", 501},
|
{"MAIL FROM:<>", 501},
|
||||||
}
|
}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test out some empty envelope with blanks
|
// Test out some empty envelope with blanks
|
||||||
script = []scriptStep{
|
script = []scriptStep{
|
||||||
{"HELO localhost", 250},
|
{"HELO localhost", 250},
|
||||||
{"MAIL FROM: <>", 501},
|
{"MAIL FROM: <>", 501},
|
||||||
}
|
}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test AUTH commands.
|
// Test AUTH commands.
|
||||||
func TestAuth(t *testing.T) {
|
func TestAuth(t *testing.T) {
|
||||||
ds := test.NewStore()
|
ds := test.NewStore()
|
||||||
server := setupSMTPServer(ds, extension.NewHost())
|
server := setupSMTPServer(ds, extension.NewHost())
|
||||||
defer server.Drain()
|
|
||||||
|
|
||||||
// PLAIN AUTH
|
// PLAIN AUTH
|
||||||
script := []scriptStep{
|
script := []scriptStep{
|
||||||
@@ -125,9 +113,7 @@ func TestAuth(t *testing.T) {
|
|||||||
{"RSET", 250},
|
{"RSET", 250},
|
||||||
{"AUTH PLAIN aW5idWNrZXQ6cG Fzc3dvcmQK", 500},
|
{"AUTH PLAIN aW5idWNrZXQ6cG Fzc3dvcmQK", 500},
|
||||||
}
|
}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LOGIN AUTH
|
// LOGIN AUTH
|
||||||
script = []scriptStep{
|
script = []scriptStep{
|
||||||
@@ -140,16 +126,13 @@ func TestAuth(t *testing.T) {
|
|||||||
{"", 334},
|
{"", 334},
|
||||||
{"", 235},
|
{"", 235},
|
||||||
}
|
}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test TLS commands.
|
// Test TLS commands.
|
||||||
func TestTLS(t *testing.T) {
|
func TestTLS(t *testing.T) {
|
||||||
ds := test.NewStore()
|
ds := test.NewStore()
|
||||||
server := setupSMTPServer(ds, extension.NewHost())
|
server := setupSMTPServer(ds, extension.NewHost())
|
||||||
defer server.Drain()
|
|
||||||
|
|
||||||
// Test Start TLS parsing.
|
// Test Start TLS parsing.
|
||||||
script := []scriptStep{
|
script := []scriptStep{
|
||||||
@@ -157,9 +140,7 @@ func TestTLS(t *testing.T) {
|
|||||||
{"STARTTLS", 454}, // TLS unconfigured.
|
{"STARTTLS", 454}, // TLS unconfigured.
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test valid commands in READY state.
|
// Test valid commands in READY state.
|
||||||
@@ -186,14 +167,11 @@ func TestReadyStateValidCommands(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.send, func(t *testing.T) {
|
t.Run(tc.send, func(t *testing.T) {
|
||||||
defer server.Drain()
|
|
||||||
script := []scriptStep{
|
script := []scriptStep{
|
||||||
{"HELO localhost", 250},
|
{"HELO localhost", 250},
|
||||||
tc,
|
tc,
|
||||||
{"QUIT", 221}}
|
{"QUIT", 221}}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,17 +190,13 @@ func TestReadyStateRejectedDomains(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.send, func(t *testing.T) {
|
t.Run(tc.send, func(t *testing.T) {
|
||||||
defer server.Drain()
|
|
||||||
script := []scriptStep{
|
script := []scriptStep{
|
||||||
{"HELO localhost", 250},
|
{"HELO localhost", 250},
|
||||||
tc,
|
tc,
|
||||||
{"QUIT", 221}}
|
{"QUIT", 221}}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test invalid commands in READY state.
|
// Test invalid commands in READY state.
|
||||||
@@ -245,24 +219,19 @@ func TestReadyStateInvalidCommands(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.send, func(t *testing.T) {
|
t.Run(tc.send, func(t *testing.T) {
|
||||||
defer server.Drain()
|
|
||||||
script := []scriptStep{
|
script := []scriptStep{
|
||||||
{"HELO localhost", 250},
|
{"HELO localhost", 250},
|
||||||
tc,
|
tc,
|
||||||
{"QUIT", 221}}
|
{"QUIT", 221}}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test commands in MAIL state
|
// Test commands in MAIL state
|
||||||
func TestMailState(t *testing.T) {
|
func TestMailState(t *testing.T) {
|
||||||
mds := test.NewStore()
|
mds := test.NewStore()
|
||||||
server := setupSMTPServer(mds, extension.NewHost())
|
server := setupSMTPServer(mds, extension.NewHost())
|
||||||
defer server.Drain()
|
|
||||||
|
|
||||||
// Test out some mangled READY commands
|
// Test out some mangled READY commands
|
||||||
script := []scriptStep{
|
script := []scriptStep{
|
||||||
@@ -278,9 +247,7 @@ func TestMailState(t *testing.T) {
|
|||||||
{"RCPT TO:<first last@host.com>", 501},
|
{"RCPT TO:<first last@host.com>", 501},
|
||||||
{"RCPT TO:<fred@fish@host.com", 501},
|
{"RCPT TO:<fred@fish@host.com", 501},
|
||||||
}
|
}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test out some good RCPT commands
|
// Test out some good RCPT commands
|
||||||
script = []scriptStep{
|
script = []scriptStep{
|
||||||
@@ -297,9 +264,7 @@ func TestMailState(t *testing.T) {
|
|||||||
{"RCPT TO:<u1@[127.0.0.1]>", 250},
|
{"RCPT TO:<u1@[127.0.0.1]>", 250},
|
||||||
{"RCPT TO:<u1@[IPv6:2001:db8:aaaa:1::100]>", 250},
|
{"RCPT TO:<u1@[IPv6:2001:db8:aaaa:1::100]>", 250},
|
||||||
}
|
}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test out recipient limit
|
// Test out recipient limit
|
||||||
script = []scriptStep{
|
script = []scriptStep{
|
||||||
@@ -312,9 +277,7 @@ func TestMailState(t *testing.T) {
|
|||||||
{"RCPT TO:<u5@gmail.com>", 250},
|
{"RCPT TO:<u5@gmail.com>", 250},
|
||||||
{"RCPT TO:<u6@gmail.com>", 552},
|
{"RCPT TO:<u6@gmail.com>", 552},
|
||||||
}
|
}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test DATA
|
// Test DATA
|
||||||
script = []scriptStep{
|
script = []scriptStep{
|
||||||
@@ -324,9 +287,7 @@ func TestMailState(t *testing.T) {
|
|||||||
{"DATA", 354},
|
{"DATA", 354},
|
||||||
{".", 250},
|
{".", 250},
|
||||||
}
|
}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test late EHLO, similar to RSET
|
// Test late EHLO, similar to RSET
|
||||||
script = []scriptStep{
|
script = []scriptStep{
|
||||||
@@ -337,9 +298,7 @@ func TestMailState(t *testing.T) {
|
|||||||
{"EHLO localhost", 250},
|
{"EHLO localhost", 250},
|
||||||
{"MAIL FROM:<john@gmail.com>", 250},
|
{"MAIL FROM:<john@gmail.com>", 250},
|
||||||
}
|
}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test RSET
|
// Test RSET
|
||||||
script = []scriptStep{
|
script = []scriptStep{
|
||||||
@@ -349,9 +308,7 @@ func TestMailState(t *testing.T) {
|
|||||||
{"RSET", 250},
|
{"RSET", 250},
|
||||||
{"MAIL FROM:<john@gmail.com>", 250},
|
{"MAIL FROM:<john@gmail.com>", 250},
|
||||||
}
|
}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test QUIT
|
// Test QUIT
|
||||||
script = []scriptStep{
|
script = []scriptStep{
|
||||||
@@ -360,16 +317,13 @@ func TestMailState(t *testing.T) {
|
|||||||
{"RCPT TO:<u1@gmail.com>", 250},
|
{"RCPT TO:<u1@gmail.com>", 250},
|
||||||
{"QUIT", 221},
|
{"QUIT", 221},
|
||||||
}
|
}
|
||||||
if err := playSession(t, server, script); err != nil {
|
playSession(t, server, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test commands in DATA state
|
// Test commands in DATA state
|
||||||
func TestDataState(t *testing.T) {
|
func TestDataState(t *testing.T) {
|
||||||
mds := test.NewStore()
|
mds := test.NewStore()
|
||||||
server := setupSMTPServer(mds, extension.NewHost())
|
server := setupSMTPServer(mds, extension.NewHost())
|
||||||
defer server.Drain()
|
|
||||||
|
|
||||||
var script []scriptStep
|
var script []scriptStep
|
||||||
pipe := setupSMTPSession(t, server)
|
pipe := setupSMTPSession(t, server)
|
||||||
@@ -384,9 +338,7 @@ func TestDataState(t *testing.T) {
|
|||||||
{"RCPT TO:<u1@gmail.com>", 250},
|
{"RCPT TO:<u1@gmail.com>", 250},
|
||||||
{"DATA", 354},
|
{"DATA", 354},
|
||||||
}
|
}
|
||||||
if err := playScriptAgainst(t, c, script); err != nil {
|
playScriptAgainst(t, c, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a message
|
// Send a message
|
||||||
body := `To: u1@gmail.com
|
body := `To: u1@gmail.com
|
||||||
@@ -416,9 +368,7 @@ Hi!
|
|||||||
{"RCPT TO:<u1@gmail.com>", 250},
|
{"RCPT TO:<u1@gmail.com>", 250},
|
||||||
{"DATA", 354},
|
{"DATA", 354},
|
||||||
}
|
}
|
||||||
if err := playScriptAgainst(t, c, script); err != nil {
|
playScriptAgainst(t, c, script)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a message
|
// Send a message
|
||||||
body = `X-Useless-Header: true
|
body = `X-Useless-Header: true
|
||||||
@@ -435,31 +385,284 @@ Hi!
|
|||||||
_, _, _ = c.ReadCodeLine(221)
|
_, _, _ = 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 {
|
||||||
|
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, // 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 {
|
||||||
|
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},
|
||||||
|
{"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
|
// 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)
|
pipe := setupSMTPSession(t, server)
|
||||||
c := textproto.NewConn(pipe)
|
c := textproto.NewConn(pipe)
|
||||||
|
|
||||||
if code, _, err := c.ReadCodeLine(220); err != nil {
|
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
|
// Not all tests leave the session in a clean state, so the following two calls can fail
|
||||||
// calls can fail
|
|
||||||
_, _ = c.Cmd("QUIT")
|
_, _ = c.Cmd("QUIT")
|
||||||
_, _, _ = c.ReadCodeLine(221)
|
_, _, _ = c.ReadCodeLine(221)
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// playScriptAgainst an existing connection, does not handle server greeting
|
// 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 {
|
for i, step := range script {
|
||||||
id, err := c.Cmd(step.send)
|
id, err := c.Cmd("%s", step.send)
|
||||||
if err != nil {
|
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)
|
c.StartResponse(id)
|
||||||
@@ -471,98 +674,10 @@ func playScriptAgainst(t *testing.T, c *textproto.Conn, script []scriptStep) err
|
|||||||
c.EndResponse(id)
|
c.EndResponse(id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Return after c.EndResponse so we don't hang the connection
|
// Fail after c.EndResponse so we don't hang the connection
|
||||||
return err
|
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
|
// net.Pipe does not implement deadlines
|
||||||
@@ -574,6 +689,7 @@ func (m *mockConn) SetDeadline(t time.Time) error { return nil }
|
|||||||
func (m *mockConn) SetReadDeadline(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 }
|
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||||
|
|
||||||
|
// Creates an unstarted smtp.Server.
|
||||||
func setupSMTPServer(ds storage.Store, extHost *extension.Host) *Server {
|
func setupSMTPServer(ds storage.Store, extHost *extension.Host) *Server {
|
||||||
cfg := &config.Root{
|
cfg := &config.Root{
|
||||||
MailboxNaming: config.FullNaming,
|
MailboxNaming: config.FullNaming,
|
||||||
@@ -589,7 +705,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}
|
addrPolicy := &policy.Addressing{Config: cfg}
|
||||||
manager := &message.StoreManager{Store: ds, ExtHost: extHost}
|
manager := &message.StoreManager{Store: ds, ExtHost: extHost}
|
||||||
|
|
||||||
@@ -599,11 +715,18 @@ func setupSMTPServer(ds storage.Store, extHost *extension.Host) *Server {
|
|||||||
var sessionNum int
|
var sessionNum int
|
||||||
|
|
||||||
func setupSMTPSession(t *testing.T, server *Server) net.Conn {
|
func setupSMTPSession(t *testing.T, server *Server) net.Conn {
|
||||||
|
t.Helper()
|
||||||
logger := zerolog.New(zerolog.NewTestWriter(t))
|
logger := zerolog.New(zerolog.NewTestWriter(t))
|
||||||
serverConn, clientConn := net.Pipe()
|
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.
|
// Start the session.
|
||||||
server.wg.Add(1)
|
|
||||||
sessionNum++
|
sessionNum++
|
||||||
go server.startSession(sessionNum, &mockConn{serverConn}, logger)
|
go server.startSession(sessionNum, &mockConn{serverConn}, logger)
|
||||||
|
|
||||||
|
|||||||
@@ -154,8 +154,8 @@ func (s *Server) serve(ctx context.Context) {
|
|||||||
} else {
|
} else {
|
||||||
tempDelay *= 2
|
tempDelay *= 2
|
||||||
}
|
}
|
||||||
if max := 1 * time.Second; tempDelay > max {
|
if maxDelay := 1 * time.Second; tempDelay > maxDelay {
|
||||||
tempDelay = max
|
tempDelay = maxDelay
|
||||||
}
|
}
|
||||||
log.Error().Str("module", "smtp").Err(err).
|
log.Error().Str("module", "smtp").Err(err).
|
||||||
Msgf("SMTP accept timeout; retrying in %v", tempDelay)
|
Msgf("SMTP accept timeout; retrying in %v", tempDelay)
|
||||||
@@ -176,8 +176,6 @@ func (s *Server) serve(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tempDelay = 0
|
tempDelay = 0
|
||||||
expConnectsTotal.Add(1)
|
|
||||||
s.wg.Add(1)
|
|
||||||
go s.startSession(sessionID, conn, log.Logger)
|
go s.startSession(sessionID, conn, log.Logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/inbucket/inbucket/v3/pkg/config"
|
|
||||||
"github.com/rs/zerolog/log"
|
"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.
|
// spaTemplateHandler creates a handler to serve the index.html template for our SPA.
|
||||||
func spaTemplateHandler(tmpl *template.Template, basePath string,
|
func spaTemplateHandler(tmpl *template.Template, basePath string) http.Handler {
|
||||||
webConfig config.Web) http.Handler {
|
|
||||||
tmplData := struct {
|
tmplData := struct {
|
||||||
BasePath string
|
BasePath string
|
||||||
}{
|
}{
|
||||||
BasePath: basePath,
|
BasePath: basePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
// ensure we do now allow click jacking
|
// ensure we do now allow click jacking
|
||||||
w.Header().Set("X-Frame-Options", "SameOrigin")
|
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
|
// WrapURL wraps a <a href> tag around the provided URL
|
||||||
func WrapURL(url string) string {
|
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)
|
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.
|
// NewServer sets up things for unit tests or the Start() method.
|
||||||
func NewServer(
|
func NewServer(conf *config.Root, mm message.Manager, mh *msghub.Hub) *Server {
|
||||||
conf *config.Root,
|
|
||||||
mm message.Manager,
|
|
||||||
mh *msghub.Hub) *Server {
|
|
||||||
|
|
||||||
rootConfig = conf
|
rootConfig = conf
|
||||||
|
|
||||||
// NewContext() will use this DataStore for the web handlers.
|
// NewContext() will use this DataStore for the web handlers.
|
||||||
@@ -113,7 +109,7 @@ func NewServer(
|
|||||||
|
|
||||||
// SPA managed paths.
|
// SPA managed paths.
|
||||||
spaHandler := cookieHandler(appConfigCookie(conf.Web),
|
spaHandler := cookieHandler(appConfigCookie(conf.Web),
|
||||||
spaTemplateHandler(indexTmpl, prefix("/"), conf.Web))
|
spaTemplateHandler(indexTmpl, prefix("/")))
|
||||||
Router.Path(prefix("/")).Handler(spaHandler)
|
Router.Path(prefix("/")).Handler(spaHandler)
|
||||||
Router.Path(prefix("/monitor")).Handler(spaHandler)
|
Router.Path(prefix("/monitor")).Handler(spaHandler)
|
||||||
Router.Path(prefix("/status")).Handler(spaHandler)
|
Router.Path(prefix("/status")).Handler(spaHandler)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package file
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -55,7 +56,7 @@ type Store struct {
|
|||||||
func New(cfg config.Storage, extHost *extension.Host) (storage.Store, error) {
|
func New(cfg config.Storage, extHost *extension.Host) (storage.Store, error) {
|
||||||
path := cfg.Params["path"]
|
path := cfg.Params["path"]
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return nil, fmt.Errorf("'path' parameter not specified")
|
return nil, errors.New("'path' parameter not specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
mailPath := getMailPath(path)
|
mailPath := getMailPath(path)
|
||||||
|
|||||||
@@ -2,20 +2,15 @@ package file
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/mail"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/inbucket/inbucket/v3/pkg/config"
|
"github.com/inbucket/inbucket/v3/pkg/config"
|
||||||
"github.com/inbucket/inbucket/v3/pkg/extension"
|
"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/storage"
|
||||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -38,7 +33,7 @@ func TestSuite(t *testing.T) {
|
|||||||
func TestFSNew(t *testing.T) {
|
func TestFSNew(t *testing.T) {
|
||||||
// Should fail if no path specified.
|
// Should fail if no path specified.
|
||||||
ds, err := New(config.Storage{}, extension.NewHost())
|
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)
|
assert.Nil(t, ds)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +68,7 @@ func TestFSDirStructure(t *testing.T) {
|
|||||||
assert.False(t, isDir(expect), "Expected %q to not exist", expect)
|
assert.False(t, isDir(expect), "Expected %q to not exist", expect)
|
||||||
|
|
||||||
// Deliver test message
|
// Deliver test message
|
||||||
id1, _ := deliverMessage(ds, mbName, "test", time.Now())
|
id1, _ := test.DeliverToStore(t, ds, mbName, "test", time.Now())
|
||||||
|
|
||||||
// Check path to message exists
|
// Check path to message exists
|
||||||
assert.True(t, isDir(expect), "Expected %q to be a directory", expect)
|
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)
|
assert.True(t, isFile(expect), "Expected %q to be a file", expect)
|
||||||
|
|
||||||
// Deliver second test message
|
// Deliver second test message
|
||||||
id2, _ := deliverMessage(ds, mbName, "test 2", time.Now())
|
id2, _ := test.DeliverToStore(t, ds, mbName, "test 2", time.Now())
|
||||||
|
|
||||||
// Check files
|
// Check files
|
||||||
expect = filepath.Join(mbPath, "index.gob")
|
expect = filepath.Join(mbPath, "index.gob")
|
||||||
@@ -100,7 +95,7 @@ func TestFSDirStructure(t *testing.T) {
|
|||||||
|
|
||||||
// Delete message
|
// Delete message
|
||||||
err := ds.RemoveMessage(mbName, id1)
|
err := ds.RemoveMessage(mbName, id1)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Message should be removed
|
// Message should be removed
|
||||||
expect = filepath.Join(mbPath, id1+".raw")
|
expect = filepath.Join(mbPath, id1+".raw")
|
||||||
@@ -110,7 +105,7 @@ func TestFSDirStructure(t *testing.T) {
|
|||||||
|
|
||||||
// Delete message
|
// Delete message
|
||||||
err = ds.RemoveMessage(mbName, id2)
|
err = ds.RemoveMessage(mbName, id2)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Message should be removed
|
// Message should be removed
|
||||||
expect = filepath.Join(mbPath, id2+".raw")
|
expect = filepath.Join(mbPath, id2+".raw")
|
||||||
@@ -141,21 +136,21 @@ func TestFSMissing(t *testing.T) {
|
|||||||
|
|
||||||
for i, subj := range subjects {
|
for i, subj := range subjects {
|
||||||
// Add a message
|
// Add a message
|
||||||
id, _ := deliverMessage(ds, mbName, subj, time.Now())
|
id, _ := test.DeliverToStore(t, ds, mbName, subj, time.Now())
|
||||||
sentIds[i] = id
|
sentIds[i] = id
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete a message file without removing it from index
|
// Delete a message file without removing it from index
|
||||||
msg, err := ds.GetMessage(mbName, sentIds[1])
|
msg, err := ds.GetMessage(mbName, sentIds[1])
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
fmsg := msg.(*Message)
|
fmsg := msg.(*Message)
|
||||||
_ = os.Remove(fmsg.rawPath())
|
_ = os.Remove(fmsg.rawPath())
|
||||||
msg, err = ds.GetMessage(mbName, sentIds[1])
|
msg, err = ds.GetMessage(mbName, sentIds[1])
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Try to read parts of message
|
// Try to read parts of message
|
||||||
_, err = msg.Source()
|
_, err = msg.Source()
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
if t.Failed() {
|
if t.Failed() {
|
||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
@@ -176,29 +171,29 @@ func TestGetLatestMessage(t *testing.T) {
|
|||||||
// Test empty mailbox
|
// Test empty mailbox
|
||||||
msg, err := ds.GetMessage(mbName, "latest")
|
msg, err := ds.GetMessage(mbName, "latest")
|
||||||
assert.Nil(t, msg)
|
assert.Nil(t, msg)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
// Deliver test message
|
// Deliver test message
|
||||||
deliverMessage(ds, mbName, "test", time.Now())
|
test.DeliverToStore(t, ds, mbName, "test", time.Now())
|
||||||
|
|
||||||
// Deliver test message 2
|
// 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
|
// Test get the latest message
|
||||||
msg, err = ds.GetMessage(mbName, "latest")
|
msg, err = ds.GetMessage(mbName, "latest")
|
||||||
require.NoError(t, err)
|
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
|
// 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")
|
msg, err = ds.GetMessage(mbName, "latest")
|
||||||
require.NoError(t, err)
|
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
|
// Test wrong id
|
||||||
_, err = ds.GetMessage(mbName, "wrongid")
|
_, err = ds.GetMessage(mbName, "wrongid")
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
if t.Failed() {
|
if t.Failed() {
|
||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
@@ -231,30 +226,6 @@ func setupDataStore(cfg config.Storage, extHost *extension.Host) (*Store, *bytes
|
|||||||
return s.(*Store), buf
|
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) {
|
func teardownDataStore(ds *Store) {
|
||||||
if err := os.RemoveAll(ds.path); err != nil {
|
if err := os.RemoveAll(ds.path); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ func (mb *mbox) readIndex() error {
|
|||||||
log.Debug().Str("module", "storage").Str("path", mb.indexPath).
|
log.Debug().Str("module", "storage").Str("path", mb.indexPath).
|
||||||
Msg("Index does not yet exist")
|
Msg("Index does not yet exist")
|
||||||
mb.indexLoaded = true
|
mb.indexLoaded = true
|
||||||
|
|
||||||
|
//lint:ignore nilerr missing mailboxes are considered empty.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
file, err := os.Open(mb.indexPath)
|
file, err := os.Open(mb.indexPath)
|
||||||
@@ -130,7 +132,7 @@ func (mb *mbox) readIndex() error {
|
|||||||
dec := gob.NewDecoder(br)
|
dec := gob.NewDecoder(br)
|
||||||
name := ""
|
name := ""
|
||||||
if err = dec.Decode(&name); err != nil {
|
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
|
mb.name = name
|
||||||
for {
|
for {
|
||||||
@@ -140,7 +142,7 @@ func (mb *mbox) readIndex() error {
|
|||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return fmt.Errorf("Corrupt mailbox %q: %v", mb.indexPath, err)
|
return fmt.Errorf("corrupt mailbox %q: %v", mb.indexPath, err)
|
||||||
}
|
}
|
||||||
msg.mailbox = mb
|
msg.mailbox = mb
|
||||||
mb.messages = append(mb.messages, msg)
|
mb.messages = append(mb.messages, msg)
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ import (
|
|||||||
// access in most cases without requiring an infinite number of mutexes.
|
// access in most cases without requiring an infinite number of mutexes.
|
||||||
type HashLock [4096]sync.RWMutex
|
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.
|
// string of three or more characters.
|
||||||
func (h *HashLock) Get(hash string) *sync.RWMutex {
|
func (h *HashLock) Get(hash string) *sync.RWMutex {
|
||||||
if len(hash) < 3 {
|
if len(hash) < 3 {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func TestHashLock(t *testing.T) {
|
|||||||
t.Run(ts, func(t *testing.T) {
|
t.Run(ts, func(t *testing.T) {
|
||||||
l := hl.Get(ts)
|
l := hl.Get(ts)
|
||||||
if l == nil {
|
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{}
|
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) {
|
func New(cfg config.Storage, extHost *extension.Host) (storage.Store, error) {
|
||||||
s := &Store{
|
s := &Store{
|
||||||
boxes: make(map[string]*mbox),
|
boxes: make(map[string]*mbox),
|
||||||
@@ -82,6 +82,7 @@ func (s *Store) AddMessage(message storage.Message) (id string, err error) {
|
|||||||
m.id = id
|
m.id = id
|
||||||
m.source = source
|
m.source = source
|
||||||
mb.messages[id] = m
|
mb.messages[id] = m
|
||||||
|
|
||||||
if s.cap > 0 {
|
if s.cap > 0 {
|
||||||
// Enforce cap.
|
// Enforce cap.
|
||||||
for len(mb.messages) > s.cap {
|
for len(mb.messages) > s.cap {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package stringutil
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"fmt"
|
"encoding/hex"
|
||||||
"io"
|
"io"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -13,10 +13,11 @@ import (
|
|||||||
func HashMailboxName(mailbox string) string {
|
func HashMailboxName(mailbox string) string {
|
||||||
h := sha1.New()
|
h := sha1.New()
|
||||||
if _, err := io.WriteString(h, mailbox); err != nil {
|
if _, err := io.WriteString(h, mailbox); err != nil {
|
||||||
// This shouldn't ever happen
|
// This should never happen.
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
|
||||||
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringAddress converts an Address to a UTF-8 string.
|
// StringAddress converts an Address to a UTF-8 string.
|
||||||
@@ -95,11 +96,9 @@ func MatchWithWildcards(p string, s string) bool {
|
|||||||
if runePattern[j-1] == '*' {
|
if runePattern[j-1] == '*' {
|
||||||
isMatchingMatrix[0][j] = isMatchingMatrix[0][j-1]
|
isMatchingMatrix[0][j] = isMatchingMatrix[0][j-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
for i := 1; i <= lenInput; i++ {
|
for i := 1; i <= lenInput; i++ {
|
||||||
for j := 1; j <= lenPattern; j++ {
|
for j := 1; j <= lenPattern; j++ {
|
||||||
|
|
||||||
if runePattern[j-1] == '*' {
|
if runePattern[j-1] == '*' {
|
||||||
isMatchingMatrix[i][j] = isMatchingMatrix[i-1][j] || isMatchingMatrix[i][j-1]
|
isMatchingMatrix[i][j] = isMatchingMatrix[i-1][j] || isMatchingMatrix[i][j-1]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,17 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/inbucket/inbucket/v3/pkg/stringutil"
|
"github.com/inbucket/inbucket/v3/pkg/stringutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHashMailboxName(t *testing.T) {
|
func TestHashMailboxName(t *testing.T) {
|
||||||
want := "1d6e1cf70ec6f9ab28d3ea4b27a49a77654d370e"
|
want := "da39a3ee5e6b4b0d3255bfef95601890afd80709"
|
||||||
got := stringutil.HashMailboxName("mail")
|
got := stringutil.HashMailboxName("")
|
||||||
if got != want {
|
assert.Equal(t, want, got, "for empty string")
|
||||||
t.Errorf("Got %q, want %q", got, want)
|
|
||||||
}
|
want = "1d6e1cf70ec6f9ab28d3ea4b27a49a77654d370e"
|
||||||
|
got = stringutil.HashMailboxName("mail")
|
||||||
|
assert.Equal(t, want, got, "for 'mail'")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStringAddressList(t *testing.T) {
|
func TestStringAddressList(t *testing.T) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/jhillyerd/goldiff"
|
"github.com/jhillyerd/goldiff"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -35,172 +36,128 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Add suites for domain and full addressing modes.
|
// TODO: Add suites for domain and full addressing modes.
|
||||||
|
type IntegrationSuite struct {
|
||||||
func TestSuite(t *testing.T) {
|
suite.Suite
|
||||||
stopServer, err := startServer()
|
stopServer func()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
client, err := client.New(restBaseURL)
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
from := "fromuser@inbucket.org"
|
from := "fromuser@inbucket.org"
|
||||||
to := []string{"recipient@inbucket.org"}
|
to := []string{"recipient@inbucket.org"}
|
||||||
input := readTestData("basic.txt")
|
input := readTestData("basic.txt")
|
||||||
|
|
||||||
// Send mail.
|
// Send mail.
|
||||||
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm receipt.
|
// Confirm receipt.
|
||||||
msg, err := client.GetMessage("recipient", "latest")
|
msg, err := client.GetMessage("recipient", "latest")
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
s.NotNil(msg)
|
||||||
}
|
|
||||||
if msg == nil {
|
|
||||||
t.Errorf("Got nil message, wanted non-nil message.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare to golden.
|
// Compare to golden.
|
||||||
got := formatMessage(msg)
|
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)
|
client, err := client.New(restBaseURL)
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
from := "fromuser@inbucket.org"
|
from := "fromuser@inbucket.org"
|
||||||
to := []string{"recipient@inbucket.org"}
|
to := []string{"recipient@inbucket.org"}
|
||||||
input := readTestData("fullname.txt")
|
input := readTestData("fullname.txt")
|
||||||
|
|
||||||
// Send mail.
|
// Send mail.
|
||||||
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm receipt.
|
// Confirm receipt.
|
||||||
msg, err := client.GetMessage("recipient", "latest")
|
msg, err := client.GetMessage("recipient", "latest")
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
s.NotNil(msg)
|
||||||
}
|
|
||||||
if msg == nil {
|
|
||||||
t.Errorf("Got nil message, wanted non-nil message.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare to golden.
|
// Compare to golden.
|
||||||
got := formatMessage(msg)
|
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)
|
client, err := client.New(restBaseURL)
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
from := "fromuser@inbucket.org"
|
from := "fromuser@inbucket.org"
|
||||||
to := []string{"recipient@inbucket.org"}
|
to := []string{"recipient@inbucket.org"}
|
||||||
input := readTestData("encodedheader.txt")
|
input := readTestData("encodedheader.txt")
|
||||||
|
|
||||||
// Send mail.
|
// Send mail.
|
||||||
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm receipt.
|
// Confirm receipt.
|
||||||
msg, err := client.GetMessage("recipient", "latest")
|
msg, err := client.GetMessage("recipient", "latest")
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
s.NotNil(msg)
|
||||||
}
|
|
||||||
if msg == nil {
|
|
||||||
t.Errorf("Got nil message, wanted non-nil message.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare to golden.
|
// Compare to golden.
|
||||||
got := formatMessage(msg)
|
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)
|
client, err := client.New(restBaseURL)
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
from := "fromuser@inbucket.org"
|
from := "fromuser@inbucket.org"
|
||||||
to := []string{"ip4recipient@[192.168.123.123]"}
|
to := []string{"ip4recipient@[192.168.123.123]"}
|
||||||
input := readTestData("no-to.txt")
|
input := readTestData("no-to.txt")
|
||||||
|
|
||||||
// Send mail.
|
// Send mail.
|
||||||
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm receipt.
|
// Confirm receipt.
|
||||||
msg, err := client.GetMessage("ip4recipient", "latest")
|
msg, err := client.GetMessage("ip4recipient", "latest")
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
s.NotNil(msg)
|
||||||
}
|
|
||||||
if msg == nil {
|
|
||||||
t.Errorf("Got nil message, wanted non-nil message.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare to golden.
|
// Compare to golden.
|
||||||
got := formatMessage(msg)
|
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)
|
client, err := client.New(restBaseURL)
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
from := "fromuser@inbucket.org"
|
from := "fromuser@inbucket.org"
|
||||||
to := []string{"ip6recipient@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]"}
|
to := []string{"ip6recipient@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]"}
|
||||||
input := readTestData("no-to.txt")
|
input := readTestData("no-to.txt")
|
||||||
|
|
||||||
// Send mail.
|
// Send mail.
|
||||||
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
err = smtpclient.SendMail(smtpHost, nil, from, to, input)
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm receipt.
|
// Confirm receipt.
|
||||||
msg, err := client.GetMessage("ip6recipient", "latest")
|
msg, err := client.GetMessage("ip6recipient", "latest")
|
||||||
if err != nil {
|
s.Require().NoError(err)
|
||||||
t.Fatal(err)
|
s.NotNil(msg)
|
||||||
}
|
|
||||||
if msg == nil {
|
|
||||||
t.Errorf("Got nil message, wanted non-nil message.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare to golden.
|
// Compare to golden.
|
||||||
got := formatMessage(msg)
|
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 {
|
func formatMessage(m *client.Message) []byte {
|
||||||
@@ -282,8 +239,7 @@ func clearEnv() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Backup ciritcal env variables.
|
// Backup ciritcal env variables.
|
||||||
switch runtime.GOOS {
|
if runtime.GOOS == "windows" {
|
||||||
case "windows":
|
|
||||||
backup("SYSTEMROOT")
|
backup("SYSTEMROOT")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+119
-147
@@ -22,11 +22,19 @@ import (
|
|||||||
type StoreFactory func(
|
type StoreFactory func(
|
||||||
config.Storage, *extension.Host) (store storage.Store, destroy func(), err error)
|
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.
|
// StoreSuite runs a set of general tests on the provided Store.
|
||||||
func StoreSuite(t *testing.T, factory StoreFactory) {
|
func StoreSuite(t *testing.T, factory StoreFactory) {
|
||||||
|
t.Helper()
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
test func(*testing.T, storage.Store, *extension.Host)
|
test func(storeSuite)
|
||||||
conf config.Storage
|
conf config.Storage
|
||||||
}{
|
}{
|
||||||
{"metadata", testMetadata, config.Storage{}},
|
{"metadata", testMetadata, config.Storage{}},
|
||||||
@@ -49,14 +57,20 @@ func StoreSuite(t *testing.T, factory StoreFactory) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
tc.test(t, store, extHost)
|
defer destroy()
|
||||||
destroy()
|
|
||||||
|
s := storeSuite{
|
||||||
|
T: t,
|
||||||
|
store: store,
|
||||||
|
extHost: extHost,
|
||||||
|
}
|
||||||
|
tc.test(s)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// testMetadata verifies message metadata is stored and retrieved correctly.
|
// 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"
|
mailbox := "testmailbox"
|
||||||
from := &mail.Address{Name: "From Person", Address: "from@person.com"}
|
from := &mail.Address{Name: "From Person", Address: "from@person.com"}
|
||||||
to := []*mail.Address{
|
to := []*mail.Address{
|
||||||
@@ -78,52 +92,52 @@ func testMetadata(t *testing.T, store storage.Store, extHost *extension.Host) {
|
|||||||
},
|
},
|
||||||
Reader: strings.NewReader(content),
|
Reader: strings.NewReader(content),
|
||||||
}
|
}
|
||||||
id, err := store.AddMessage(delivery)
|
id, err := s.store.AddMessage(delivery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
s.Fatal(err)
|
||||||
}
|
}
|
||||||
if id == "" {
|
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.
|
// Retrieve and validate the message.
|
||||||
sm, err := store.GetMessage(mailbox, id)
|
sm, err := s.store.GetMessage(mailbox, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
s.Fatal(err)
|
||||||
}
|
}
|
||||||
if sm.Mailbox() != mailbox {
|
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 {
|
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 {
|
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) {
|
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 {
|
} else {
|
||||||
for i, got := range sm.To() {
|
for i, got := range sm.To() {
|
||||||
if *to[i] != *got {
|
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) {
|
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 {
|
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)) {
|
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() {
|
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.
|
// 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)
|
content := make([]byte, 5000)
|
||||||
for i := 0; i < len(content); i++ {
|
for i := 0; i < len(content); i++ {
|
||||||
content[i] = byte(i % 256)
|
content[i] = byte(i % 256)
|
||||||
@@ -146,332 +160,290 @@ func testContent(t *testing.T, store storage.Store, extHost *extension.Host) {
|
|||||||
},
|
},
|
||||||
Reader: bytes.NewReader(content),
|
Reader: bytes.NewReader(content),
|
||||||
}
|
}
|
||||||
id, err := store.AddMessage(delivery)
|
id, err := s.store.AddMessage(delivery)
|
||||||
require.NoError(t, err, "AddMessage() failed")
|
require.NoError(s, err, "AddMessage() failed")
|
||||||
|
|
||||||
// Read stored message source.
|
// Read stored message source.
|
||||||
m, err := store.GetMessage(mailbox, id)
|
m, err := s.store.GetMessage(mailbox, id)
|
||||||
require.NoError(t, err, "GetMessage() failed")
|
require.NoError(s, err, "GetMessage() failed")
|
||||||
r, err := m.Source()
|
r, err := m.Source()
|
||||||
require.NoError(t, err, "Source() failed")
|
require.NoError(s, err, "Source() failed")
|
||||||
got, err := io.ReadAll(r)
|
got, err := io.ReadAll(r)
|
||||||
require.NoError(t, err, "failed to read source")
|
require.NoError(s, err, "failed to read source")
|
||||||
err = r.Close()
|
err = r.Close()
|
||||||
assert.NoError(t, err, "failed to close source reader")
|
require.NoError(s, err, "failed to close source reader")
|
||||||
|
|
||||||
// Verify source.
|
// Verify source.
|
||||||
if len(got) != len(content) {
|
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
|
errors := 0
|
||||||
for i, b := range got {
|
for i, b := range got {
|
||||||
if b != content[i] {
|
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++
|
errors++
|
||||||
}
|
}
|
||||||
if errors > 5 {
|
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
|
// testDeliveryOrder delivers several messages to the same mailbox, meanwhile querying its contents
|
||||||
// with a new GetMessages call each cycle.
|
// with a new GetMessages call each cycle.
|
||||||
func testDeliveryOrder(t *testing.T, store storage.Store, extHost *extension.Host) {
|
func testDeliveryOrder(s storeSuite) {
|
||||||
mailbox := "fred"
|
mailbox := "fred"
|
||||||
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
||||||
for i, subj := range subjects {
|
for i, subj := range subjects {
|
||||||
// Check mailbox count.
|
// Check mailbox count.
|
||||||
GetAndCountMessages(t, store, mailbox, i)
|
GetAndCountMessages(s.T, s.store, mailbox, i)
|
||||||
DeliverToStore(t, store, mailbox, subj, time.Now())
|
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
||||||
}
|
}
|
||||||
// Confirm delivery order.
|
// Confirm delivery order.
|
||||||
msgs := GetAndCountMessages(t, store, mailbox, 5)
|
msgs := GetAndCountMessages(s.T, s.store, mailbox, 5)
|
||||||
for i, want := range subjects {
|
for i, want := range subjects {
|
||||||
got := msgs[i].Subject()
|
got := msgs[i].Subject()
|
||||||
if got != want {
|
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
|
// testLatest delivers several messages to the same mailbox, and confirms the id `latest` returns
|
||||||
// the last message sent.
|
// the last message sent.
|
||||||
func testLatest(t *testing.T, store storage.Store, extHost *extension.Host) {
|
func testLatest(s storeSuite) {
|
||||||
mailbox := "fred"
|
mailbox := "fred"
|
||||||
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
||||||
for _, subj := range subjects {
|
for _, subj := range subjects {
|
||||||
DeliverToStore(t, store, mailbox, subj, time.Now())
|
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
||||||
}
|
}
|
||||||
// Confirm latest.
|
// Confirm latest.
|
||||||
latest, err := store.GetMessage(mailbox, "latest")
|
latest, err := s.store.GetMessage(mailbox, "latest")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
s.Fatal(err)
|
||||||
}
|
}
|
||||||
if latest == nil {
|
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()
|
got := latest.Subject()
|
||||||
want := "echo"
|
want := "echo"
|
||||||
if got != want {
|
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.
|
// testNaming ensures the store does not enforce local part mailbox naming.
|
||||||
func testNaming(t *testing.T, store storage.Store, extHost *extension.Host) {
|
func testNaming(s storeSuite) {
|
||||||
DeliverToStore(t, store, "fred@fish.net", "disk #27", time.Now())
|
DeliverToStore(s.T, s.store, "fred@fish.net", "disk #27", time.Now())
|
||||||
GetAndCountMessages(t, store, "fred", 0)
|
GetAndCountMessages(s.T, s.store, "fred", 0)
|
||||||
GetAndCountMessages(t, store, "fred@fish.net", 1)
|
GetAndCountMessages(s.T, s.store, "fred@fish.net", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// testSize verifies message content size metadata values.
|
// testSize verifies message content size metadata values.
|
||||||
func testSize(t *testing.T, store storage.Store, extHost *extension.Host) {
|
func testSize(s storeSuite) {
|
||||||
mailbox := "fred"
|
mailbox := "fred"
|
||||||
subjects := []string{"a", "br", "much longer than the others"}
|
subjects := []string{"a", "br", "much longer than the others"}
|
||||||
sentIds := make([]string, len(subjects))
|
sentIds := make([]string, len(subjects))
|
||||||
sentSizes := make([]int64, len(subjects))
|
sentSizes := make([]int64, len(subjects))
|
||||||
for i, subj := range 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
|
sentIds[i] = id
|
||||||
sentSizes[i] = size
|
sentSizes[i] = size
|
||||||
}
|
}
|
||||||
for i, id := range sentIds {
|
for i, id := range sentIds {
|
||||||
msg, err := store.GetMessage(mailbox, id)
|
msg, err := s.store.GetMessage(mailbox, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
s.Fatal(err)
|
||||||
}
|
}
|
||||||
want := sentSizes[i]
|
want := sentSizes[i]
|
||||||
got := msg.Size()
|
got := msg.Size()
|
||||||
if got != want {
|
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.
|
// 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"
|
mailbox := "lisa"
|
||||||
id1, _ := DeliverToStore(t, store, mailbox, "whatever", time.Now())
|
id1, _ := DeliverToStore(s.T, s.store, mailbox, "whatever", time.Now())
|
||||||
id2, _ := DeliverToStore(t, store, mailbox, "hello?", time.Now())
|
id2, _ := DeliverToStore(s.T, s.store, mailbox, "hello?", time.Now())
|
||||||
// Confirm unseen.
|
// Confirm unseen.
|
||||||
msg, err := store.GetMessage(mailbox, id1)
|
msg, err := s.store.GetMessage(mailbox, id1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
s.Fatal(err)
|
||||||
}
|
}
|
||||||
if msg.Seen() {
|
if msg.Seen() {
|
||||||
t.Errorf("got seen %v, want: false", msg.Seen())
|
s.Errorf("got seen %v, want: false", msg.Seen())
|
||||||
}
|
}
|
||||||
// Mark id1 seen.
|
// Mark id1 seen.
|
||||||
err = store.MarkSeen(mailbox, id1)
|
err = s.store.MarkSeen(mailbox, id1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
s.Fatal(err)
|
||||||
}
|
}
|
||||||
// Verify id1 seen.
|
// Verify id1 seen.
|
||||||
msg, err = store.GetMessage(mailbox, id1)
|
msg, err = s.store.GetMessage(mailbox, id1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
s.Fatal(err)
|
||||||
}
|
}
|
||||||
if !msg.Seen() {
|
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.
|
// Verify id2 still unseen.
|
||||||
msg, err = store.GetMessage(mailbox, id2)
|
msg, err = s.store.GetMessage(mailbox, id2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
s.Fatal(err)
|
||||||
}
|
}
|
||||||
if msg.Seen() {
|
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.
|
// testDelete creates and deletes some messages.
|
||||||
func testDelete(t *testing.T, store storage.Store, extHost *extension.Host) {
|
func testDelete(s storeSuite) {
|
||||||
mailbox := "fred"
|
mailbox := "fred"
|
||||||
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
||||||
for _, subj := range subjects {
|
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.
|
// Subscribe to events.
|
||||||
eventListener := extHost.Events.AfterMessageDeleted.AsyncTestListener("test", 2)
|
eventListener := s.extHost.Events.AfterMessageDeleted.AsyncTestListener("test", 2)
|
||||||
|
|
||||||
// Delete a couple messages.
|
// Delete a couple messages.
|
||||||
deleteIDs := []string{msgs[1].ID(), msgs[3].ID()}
|
deleteIDs := []string{msgs[1].ID(), msgs[3].ID()}
|
||||||
for _, id := range deleteIDs {
|
for _, id := range deleteIDs {
|
||||||
err := store.RemoveMessage(mailbox, id)
|
err := s.store.RemoveMessage(mailbox, id)
|
||||||
require.NoError(t, err)
|
require.NoError(s, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm deletion.
|
// Confirm deletion.
|
||||||
subjects = []string{"alpha", "charlie", "echo"}
|
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 {
|
for i, want := range subjects {
|
||||||
got := msgs[i].Subject()
|
got := msgs[i].Subject()
|
||||||
if got != want {
|
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.
|
// Capture events and check correct IDs were emitted.
|
||||||
ev1, err := eventListener()
|
ev1, err := eventListener()
|
||||||
require.NoError(t, err)
|
require.NoError(s, err)
|
||||||
ev2, err := eventListener()
|
ev2, err := eventListener()
|
||||||
require.NoError(t, err)
|
require.NoError(s, err)
|
||||||
eventIDs := []string{ev1.ID, ev2.ID}
|
eventIDs := []string{ev1.ID, ev2.ID}
|
||||||
for _, id := range deleteIDs {
|
for _, id := range deleteIDs {
|
||||||
assert.Contains(t, eventIDs, id)
|
assert.Contains(s, eventIDs, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try appending one more.
|
// 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"}
|
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 {
|
for i, want := range subjects {
|
||||||
got := msgs[i].Subject()
|
got := msgs[i].Subject()
|
||||||
if got != want {
|
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.
|
// testPurge makes sure mailboxes can be purged.
|
||||||
func testPurge(t *testing.T, store storage.Store, extHost *extension.Host) {
|
func testPurge(s storeSuite) {
|
||||||
mailbox := "fred"
|
mailbox := "fred"
|
||||||
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
||||||
|
|
||||||
// Subscribe to events.
|
// Subscribe to events.
|
||||||
eventListener := extHost.Events.AfterMessageDeleted.AsyncTestListener("test", len(subjects))
|
eventListener := s.extHost.Events.AfterMessageDeleted.AsyncTestListener("test", len(subjects))
|
||||||
|
|
||||||
// Populate mailbox.
|
// Populate mailbox.
|
||||||
for _, subj := range subjects {
|
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.
|
// Purge and verify.
|
||||||
err := store.PurgeMessages(mailbox)
|
err := s.store.PurgeMessages(mailbox)
|
||||||
require.NoError(t, err)
|
require.NoError(s, err)
|
||||||
GetAndCountMessages(t, store, mailbox, 0)
|
GetAndCountMessages(s.T, s.store, mailbox, 0)
|
||||||
|
|
||||||
// Confirm events emitted.
|
// Confirm events emitted.
|
||||||
gotEvents := []*event.MessageMetadata{}
|
gotEvents := []*event.MessageMetadata{}
|
||||||
for range subjects {
|
for range subjects {
|
||||||
ev, err := eventListener()
|
ev, err := eventListener()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
s.Error(err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
gotEvents = append(gotEvents, ev)
|
gotEvents = append(gotEvents, ev)
|
||||||
}
|
}
|
||||||
assert.Equal(t, len(subjects), len(gotEvents),
|
assert.Equal(s, len(subjects), len(gotEvents),
|
||||||
"expected delete event for each message in mailbox")
|
"expected delete event for each message in mailbox")
|
||||||
}
|
}
|
||||||
|
|
||||||
// testMsgCap verifies the message cap is enforced.
|
// testMsgCap verifies the message cap is enforced.
|
||||||
func testMsgCap(t *testing.T, store storage.Store, extHost *extension.Host) {
|
func testMsgCap(s storeSuite) {
|
||||||
mbCap := 10
|
mbCap := 10
|
||||||
mailbox := "captain"
|
mailbox := "captain"
|
||||||
|
|
||||||
for i := 0; i < 20; i++ {
|
for i := 0; i < 20; i++ {
|
||||||
subj := fmt.Sprintf("subject %v", i)
|
subj := fmt.Sprintf("subject %v", i)
|
||||||
DeliverToStore(t, store, mailbox, subj, time.Now())
|
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
||||||
msgs, err := store.GetMessages(mailbox)
|
msgs, err := s.store.GetMessages(mailbox)
|
||||||
if err != nil {
|
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 {
|
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
|
break
|
||||||
}
|
}
|
||||||
// Check that the first message is correct.
|
|
||||||
|
// Check that the first (oldest) message is correct.
|
||||||
first := i - mbCap + 1
|
first := i - mbCap + 1
|
||||||
if first < 0 {
|
if first < 0 {
|
||||||
first = 0
|
first = 0
|
||||||
}
|
}
|
||||||
firstSubj := fmt.Sprintf("subject %v", first)
|
firstSubj := fmt.Sprintf("subject %v", first)
|
||||||
if firstSubj != msgs[0].Subject() {
|
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.
|
// 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"
|
mailbox := "captain"
|
||||||
for i := 0; i < 20; i++ {
|
for i := 0; i < 20; i++ {
|
||||||
subj := fmt.Sprintf("subject %v", i)
|
subj := fmt.Sprintf("subject %v", i)
|
||||||
DeliverToStore(t, store, mailbox, subj, time.Now())
|
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
||||||
GetAndCountMessages(t, store, mailbox, i+1)
|
GetAndCountMessages(s.T, s.store, mailbox, i+1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// testVisitMailboxes creates some mailboxes and confirms the VisitMailboxes method visits all of
|
// testVisitMailboxes creates some mailboxes and confirms the VisitMailboxes method visits all of
|
||||||
// them.
|
// 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.
|
// Deliver 2 test messages to each of 5 mailboxes.
|
||||||
boxes := []string{"abby", "bill", "christa", "donald", "evelyn"}
|
boxes := []string{"abby", "bill", "christa", "donald", "evelyn"}
|
||||||
for _, name := range boxes {
|
for _, name := range boxes {
|
||||||
DeliverToStore(t, ds, name, "Old Message", time.Now().Add(-24*time.Hour))
|
DeliverToStore(s.T, s.store, name, "Old Message", time.Now().Add(-24*time.Hour))
|
||||||
DeliverToStore(t, ds, name, "New Message", time.Now())
|
DeliverToStore(s.T, s.store, name, "New Message", time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify message and mailbox counts.
|
// Verify message and mailbox counts.
|
||||||
nboxes := 0
|
nboxes := 0
|
||||||
err := ds.VisitMailboxes(func(messages []storage.Message) bool {
|
err := s.store.VisitMailboxes(func(messages []storage.Message) bool {
|
||||||
nboxes++
|
nboxes++
|
||||||
name := "unknown"
|
name := "unknown"
|
||||||
if len(messages) > 0 {
|
if len(messages) > 0 {
|
||||||
name = messages[0].Mailbox()
|
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
|
return true
|
||||||
})
|
})
|
||||||
assert.NoError(t, err, "VisitMailboxes() failed")
|
require.NoError(s, err, "VisitMailboxes() failed")
|
||||||
assert.Equal(t, 5, nboxes, "visited %v mailboxes, want: 5", nboxes)
|
assert.Equal(s, 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
|
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -84,7 +84,7 @@ func TestStoreStubMailboxAddGetVisit(t *testing.T) {
|
|||||||
|
|
||||||
want, ok := expectCounts[mailbox]
|
want, ok := expectCounts[mailbox]
|
||||||
assert.True(t, ok, "Mailbox %q was unexpected", 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)
|
delete(expectCounts, mailbox)
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ func TestStoreStubMarkSeen(t *testing.T) {
|
|||||||
// Mark second message as seen.
|
// Mark second message as seen.
|
||||||
seen := inputMsgs[1]
|
seen := inputMsgs[1]
|
||||||
err := ss.MarkSeen("box1", seen.ID())
|
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.
|
// Verify message has seen flag.
|
||||||
got, err := ss.GetMessage("box1", seen.ID())
|
got, err := ss.GetMessage("box1", seen.ID())
|
||||||
@@ -150,16 +150,16 @@ func TestStoreStubRemoveMessage(t *testing.T) {
|
|||||||
// Delete second message.
|
// Delete second message.
|
||||||
deleted := inputMsgs[1]
|
deleted := inputMsgs[1]
|
||||||
err := ss.RemoveMessage("box1", deleted.ID())
|
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.
|
// Verify message is not in mailbox.
|
||||||
messages, err := ss.GetMessages("box1")
|
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())
|
assert.NotContains(t, messages, deleted, "Mailbox should not contain msg %q", deleted.ID())
|
||||||
|
|
||||||
// Verify message is no longer retrievable.
|
// Verify message is no longer retrievable.
|
||||||
got, err := ss.GetMessage("box1", deleted.ID())
|
got, err := ss.GetMessage("box1", deleted.ID())
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Nil(t, got, "Message should have been nil")
|
assert.Nil(t, got, "Message should have been nil")
|
||||||
|
|
||||||
// Verify message is in deleted list.
|
// Verify message is in deleted list.
|
||||||
@@ -181,12 +181,12 @@ func TestStoreStubPurgeMessages(t *testing.T) {
|
|||||||
|
|
||||||
// Purge messages.
|
// Purge messages.
|
||||||
err := ss.PurgeMessages("box1")
|
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.
|
// Verify message is not in mailbox.
|
||||||
messages, err := ss.GetMessages("box1")
|
messages, err := ss.GetMessages("box1")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, messages, 0, "Mailbox should be empty")
|
assert.Empty(t, messages, "Mailbox should be empty")
|
||||||
|
|
||||||
// Verify messages are in deleted list.
|
// Verify messages are in deleted list.
|
||||||
for _, want := range inputMsgs {
|
for _, want := range inputMsgs {
|
||||||
@@ -208,15 +208,15 @@ func TestStoreStubForcedErrors(t *testing.T) {
|
|||||||
|
|
||||||
// Verify methods return error.
|
// Verify methods return error.
|
||||||
_, err = ss.GetMessage("messageerr", id1)
|
_, err = ss.GetMessage("messageerr", id1)
|
||||||
assert.Error(t, err, "GetMessage()")
|
require.Error(t, err, "GetMessage()")
|
||||||
assert.NotEqual(t, storage.ErrNotExist, err)
|
assert.NotEqual(t, storage.ErrNotExist, err)
|
||||||
|
|
||||||
_, err = ss.GetMessages("messageserr")
|
_, err = ss.GetMessages("messageserr")
|
||||||
assert.Error(t, err, "GetMessages()")
|
require.Error(t, err, "GetMessages()")
|
||||||
assert.NotEqual(t, storage.ErrNotExist, err)
|
assert.NotEqual(t, storage.ErrNotExist, err)
|
||||||
|
|
||||||
err = ss.MarkSeen("messageerr", id1)
|
err = ss.MarkSeen("messageerr", id1)
|
||||||
assert.Error(t, err, "MarkSeen()")
|
require.Error(t, err, "MarkSeen()")
|
||||||
assert.NotEqual(t, storage.ErrNotExist, err)
|
assert.NotEqual(t, storage.ErrNotExist, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
func RootGreeting(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
func RootGreeting(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
||||||
greeting, err := os.ReadFile(ctx.RootConfig.Web.GreetingFile)
|
greeting, err := os.ReadFile(ctx.RootConfig.Web.GreetingFile)
|
||||||
if err != nil {
|
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")
|
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 {
|
func styleTagFilter(w io.Writer, r io.Reader) error {
|
||||||
bw := bufio.NewWriter(w)
|
bw := bufio.NewWriter(w)
|
||||||
b := make([]byte, 256)
|
b := make([]byte, 0, 256)
|
||||||
z := html.NewTokenizer(r)
|
z := html.NewTokenizer(r)
|
||||||
for {
|
for {
|
||||||
b = b[:0]
|
b = b[:0]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pkgs.mkShell {
|
|||||||
elmPackages.elm-language-server
|
elmPackages.elm-language-server
|
||||||
elmPackages.elm-test
|
elmPackages.elm-test
|
||||||
go_1_21
|
go_1_21
|
||||||
|
golangci-lint
|
||||||
golint
|
golint
|
||||||
gopls
|
gopls
|
||||||
nodejs_20
|
nodejs_20
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
"elm/time": "1.0.0",
|
"elm/time": "1.0.0",
|
||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"jweir/sparkline": "4.0.0",
|
"jweir/sparkline": "4.0.0",
|
||||||
"ryannhg/date-format": "2.3.0"
|
"ryan-haskell/date-format": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/bytes": "1.0.8",
|
"elm/bytes": "1.0.8",
|
||||||
|
|||||||
+13
-13
@@ -1035,12 +1035,12 @@ brace-expansion@^1.1.7:
|
|||||||
balanced-match "^1.0.0"
|
balanced-match "^1.0.0"
|
||||||
concat-map "0.0.1"
|
concat-map "0.0.1"
|
||||||
|
|
||||||
braces@^3.0.2:
|
braces@^3.0.3:
|
||||||
version "3.0.2"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.0.1"
|
fill-range "^7.1.1"
|
||||||
|
|
||||||
browserslist@^4.6.6:
|
browserslist@^4.6.6:
|
||||||
version "4.22.3"
|
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"
|
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||||
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
|
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
|
||||||
|
|
||||||
fill-range@^7.0.1:
|
fill-range@^7.1.1:
|
||||||
version "7.0.1"
|
version "7.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range "^5.0.1"
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
@@ -1697,11 +1697,11 @@ mdn-data@2.0.14:
|
|||||||
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
|
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
|
||||||
|
|
||||||
micromatch@^4.0.5:
|
micromatch@^4.0.5:
|
||||||
version "4.0.5"
|
version "4.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
|
||||||
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
|
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
|
||||||
dependencies:
|
dependencies:
|
||||||
braces "^3.0.2"
|
braces "^3.0.3"
|
||||||
picomatch "^2.3.1"
|
picomatch "^2.3.1"
|
||||||
|
|
||||||
minimatch@^3.1.1:
|
minimatch@^3.1.1:
|
||||||
|
|||||||
Reference in New Issue
Block a user