1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-17 09:37:02 +00:00

Compare commits

...

768 Commits

Author SHA1 Message Date
James Hillyerd
997c6307b8 chore: Update Go version to 1.25 in release workflow 2025-11-22 11:46:30 -08:00
dependabot[bot]
e0824eb0aa build(deps): bump actions/setup-node from 4 to 6 (#582)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-22 11:44:05 -08:00
dependabot[bot]
f210b4c47c build(deps): bump actions/setup-go from 5 to 6 (#580)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-22 11:38:57 -08:00
James Hillyerd
2ea0639509 chore: Update Go version in lint workflow to 1.25 (#586) 2025-11-22 11:35:33 -08:00
dependabot[bot]
4399d02f0b build(deps): bump actions/checkout from 4 to 5 (#578)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-22 10:42:57 -08:00
dependabot[bot]
25007f4506 build(deps): bump js-yaml from 4.1.0 to 4.1.1 in /ui (#584)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-22 10:42:34 -08:00
James Hillyerd
fe0e3a00e1 chore: bump go to 1.25 (#585)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2025-11-22 10:41:48 -08:00
James Hillyerd
577a329240 Changelog for v3.1.0 (#577)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2025-07-27 12:14:00 -07:00
James Hillyerd
c3a8eb8e3b fix: Note missing script is not an error (#575)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2025-07-27 11:33:04 -07:00
James Hillyerd
273c6a5dbd fix: context related lints (#576)
* fix: POP3 server now uses TLS HandshakeContext

Signed-off-by: James Hillyerd <james@hillyerd.com>

* fix: Web server now uses Listen with context

Signed-off-by: James Hillyerd <james@hillyerd.com>

* fix: replace interface{} with any

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2025-07-27 11:25:03 -07:00
James Hillyerd
f799e3debf chore: modernize range loops (#574)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2025-05-31 12:57:47 -07:00
James Hillyerd
8a1a01660c chore: update goreleaser deprecated formats (#573)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2025-05-10 12:56:16 -07:00
dependabot[bot]
d1f2ae7946 build(deps): bump base-x from 3.0.9 to 3.0.11 in /ui (#564)
Bumps [base-x](https://github.com/cryptocoinjs/base-x) from 3.0.9 to 3.0.11.
- [Commits](https://github.com/cryptocoinjs/base-x/compare/v3.0.9...v3.0.11)

---
updated-dependencies:
- dependency-name: base-x
  dependency-version: 3.0.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 08:16:15 -07:00
dependabot[bot]
8339cb5378 build(deps): bump golangci/golangci-lint-action from 7 to 8 (#565)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6 to 8.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v6...v8)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 07:39:25 -07:00
James Hillyerd
cf92969719 chore: Update golangci lint to 2.0.x (#572)
* Update to golangci lint 2.0.x

Signed-off-by: James Hillyerd <james@hillyerd.com>

* Fix new lint warnings

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2025-05-08 21:23:42 -07:00
James Hillyerd
9a2b0f934a chore: Update Go deps (#571)
* chore: Update Go deps

Signed-off-by: James Hillyerd <james@hillyerd.com>

* Fix lint warnings for loopvars

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2025-05-08 20:53:25 -07:00
James Hillyerd
b99cf9b6dc chore: Build with Go 1.24 (#568)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2025-05-08 20:41:54 -07:00
James Hillyerd
f6d00dfcb2 Update expired linters (#569)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2025-05-08 20:37:42 -07:00
Steve Atkins
440fddfe46 Add the 821.From / return-path of an email to the stored message (#560)
as a Return-Path: header. This is visible in the source view and as a
header via the REST API.

Signed-off-by: Steve Atkins <steve@wordtothewise.com>
2025-03-18 08:28:31 -07:00
Steve Atkins
9904399d24 Accept and handle emails sent with an empty 821.From / return-path as… (#561)
* Accept and handle emails sent with an empty 821.From / return-path as it would any other email.
2025-03-17 08:54:30 -07:00
Sander
4c8c8e7744 Update README.md (#556)
I copied the command from the website at [/packages/docker.html](https://inbucket.org/packages/docker.html). I thought it would be convenient if people could directly copy the command to run the image locally from the readme.
2025-02-17 09:13:57 -08:00
James Hillyerd
bd51662ce8 release 3.1.0-beta3 (#554)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-11-02 13:08:23 -07:00
James Hillyerd
7d396a6bff chore: update CHANGELOG.md (#553)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-11-02 13:00:25 -07:00
James Hillyerd
b91a681ac0 chore: Modernize Docker ENV statements (#552)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-10-20 16:53:44 -07:00
James Hillyerd
9471035a59 chore: Build with Go 1.23 (#551)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-10-20 13:05:39 -07:00
James Hillyerd
5902189187 fix: AfterMessageStored message.size (#550)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-10-20 12:32:51 -07:00
James Hillyerd
15d1970dbe feat: Add RemoteAddr to SMTPSession (#548)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-10-20 11:49:55 -07:00
James Hillyerd
78d4c4f4e7 chore: Update BeforeMailAccepted (#547)
* chore: rename BeforeMailAccepted to BeforeMailFromAccepted

Signed-off-by: James Hillyerd <james@hillyerd.com>

* chore: update BeforeMailAccepted to use SMTPSession

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-10-19 14:06:51 -07:00
James Hillyerd
9f90a59bef feat: Add SMTPSession and BeforeRcptToAccepted event (#541)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-10-13 15:29:50 -07:00
James Hillyerd
3110183a17 feat: Add SMTPResponse type for extensions (#539)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-10-05 18:16:49 -07:00
James Hillyerd
8097b3cc8a fix: ls.Get calls use top-relative index (#537)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-10-04 21:26:31 -07:00
dependabot[bot]
35549d9bf1 build(deps): bump micromatch from 4.0.5 to 4.0.8 in /ui (#527)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-29 13:19:51 -07:00
dependabot[bot]
6a679bcbc0 build(deps): bump braces from 3.0.2 to 3.0.3 in /ui (#518)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-29 13:01:11 -07:00
James Hillyerd
81bc7c2ea7 chore: test build UI outside of release (#534)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-09-29 12:49:58 -07:00
James Hillyerd
f140cf7989 chore: update goreleaser to v2 (#533)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-09-29 12:11:14 -07:00
James Hillyerd
5284171dc5 chore: create Lua test helper (#532)
* Create lua test helper

Signed-off-by: James Hillyerd <james@hillyerd.com>

* Assert labels

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-09-29 11:46:01 -07:00
James Hillyerd
b1b7e4b07c chore: use enmime 2.0 (#531)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-09-28 12:03:01 -07:00
James Hillyerd
cdff6ea571 chore: bump go deps (#529)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-09-28 11:54:31 -07:00
James Hillyerd
95ec463f26 chore: fix linter warnings (#530)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-09-28 11:49:32 -07:00
dependabot[bot]
924fb46b4e build(deps): bump goreleaser/goreleaser-action from 5 to 6 (#513)
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 5 to 6.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-28 11:45:55 -07:00
dependabot[bot]
504a79aef4 build(deps): bump docker/build-push-action from 5 to 6 (#521)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-30 08:49:09 -07:00
James Hillyerd
543c2afda5 chore: refactor proto tests to use ReadDotLines (#520)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-06-13 17:30:44 -07:00
James Hillyerd
daeba2d024 chore: disable deprecated execinquery linter (#519)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-06-13 12:44:33 -07:00
James Hillyerd
b16764a65d fix(pop3): Prevent STLS cmd triggered crashes (#516)
* fix(pop3): Prevent STLS cmd triggered crashes

Signed-off-by: James Hillyerd <james@hillyerd.com>

* err lint fix

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-06-13 12:38:07 -07:00
James Hillyerd
0df07fc1be fix: skip-pkg-cache no longer supported by golangci-lint (#517)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-06-13 12:22:14 -07:00
dependabot[bot]
9478098c0f build(deps): bump golangci/golangci-lint-action from 4 to 6 (#511)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v6)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 21:09:11 -07:00
dependabot[bot]
658506bb11 build(deps): bump golang.org/x/net from 0.20.0 to 0.23.0 (#509)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.20.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.20.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-22 11:26:02 -07:00
Roberto Rossetti
8826b8342b fix(ui): date-format version (#508)
Signed-off-by: Roberto Rossetti <robertorossetti3.14@gmail.com>
2024-04-22 11:05:18 -07:00
James Hillyerd
ffb4ce0b1b actions: configure golangci linters (#474)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-20 13:00:02 -08:00
James Hillyerd
2b174c8b0b chore: resolve error & string related lint warnings (#507)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-20 12:47:27 -08:00
James Hillyerd
5729a212ce chore: two small lint fixes (#506)
* chore: faster hash to string conv

Signed-off-by: James Hillyerd <james@hillyerd.com>

* chore: require NoError in integration test setup

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-20 12:30:04 -08:00
James Hillyerd
ed4a83a2bd chore: migrate integration to testify/suite (#505)
* fix: future naming collision, suite -> storeSuite

Signed-off-by: James Hillyerd <james@hillyerd.com>

* chore: migrate integration to testify/suite

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-19 18:45:55 -08:00
James Hillyerd
c59e793775 chore: refactor smtp/handler if-else chain (#504)
* chore: convert smtp/handler if-else chain to switch-case

Signed-off-by: James Hillyerd <james@hillyerd.com>

* chore: extract long case into parseMailCmd func

Signed-off-by: James Hillyerd <james@hillyerd.com>

* chore: remove extraneous braces in cases

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-19 17:30:37 -08:00
dependabot[bot]
40ec108daf build(deps): bump actions/checkout from 3 to 4 (#499)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 17:27:32 -08:00
dependabot[bot]
185018e001 build(deps): bump docker/setup-qemu-action from 2 to 3 (#501)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 17:19:07 -08:00
James Hillyerd
d62a0fede9 fix: prevent smtp/handler test from freezing on panic (#503)
* chore: colocate SMTP session WaitGroup incr/decr

Signed-off-by: James Hillyerd <james@hillyerd.com>

* fix: smtp tests that hang on panic/t.Fatal

Signed-off-by: James Hillyerd <james@hillyerd.com>

* chore: reorder smtp/handler test helpers

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-19 16:46:33 -08:00
James Hillyerd
25c6f58535 chore: fix testutils linter warnings (#502)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-19 12:02:06 -08:00
dependabot[bot]
e4ca20e471 build(deps): bump goreleaser/goreleaser-action from 4 to 5 (#500)
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 4 to 5.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 12:01:24 -08:00
dependabot[bot]
7fa6b38b38 build(deps): bump docker/metadata-action from 4 to 5 (#498)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 4 to 5.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Upgrade guide](https://github.com/docker/metadata-action/blob/master/UPGRADE.md)
- [Commits](https://github.com/docker/metadata-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 11:57:41 -08:00
dependabot[bot]
13ac9a6a1c build(deps): bump docker/build-push-action from 4 to 5 (#497)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 11:56:09 -08:00
James Hillyerd
0a51641a30 feature: Context support for REST client (#496)
* rest/client: add WithContext methods

Signed-off-by: James Hillyerd <james@hillyerd.com>

* cmd/client: pass context

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-17 19:25:01 -08:00
James Hillyerd
73203c6bcd fstore: remove redundant test helper (#495)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-17 18:18:30 -08:00
James Hillyerd
6066be831c chore: refactor playSession etc to use t.Fatal (#494)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-16 17:04:35 -08:00
James Hillyerd
33784cbb94 chore: more small lint/perf fixes (#493)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-16 16:53:50 -08:00
James Hillyerd
f76b93a8f2 storage_suite: refactor to use common struct param (#492)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-16 16:11:07 -08:00
James Hillyerd
0361e971e0 chore: many small lint/perf fixes (#491)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-16 14:27:04 -08:00
James Hillyerd
def3e88651 luahost: use sentinel ErrNoScript (#490)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-16 14:16:17 -08:00
James Hillyerd
8adae023dc chore: rework client example to omit log.Fatal, breaks defer (#489)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-15 19:13:03 -08:00
James Hillyerd
fc8ea530bb chore: fix many unit test style warnings (#488)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-15 18:52:50 -08:00
James Hillyerd
ea585c4851 chore: fix more capitalization style warnings (#487)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-15 18:31:43 -08:00
James Hillyerd
baa2dbd3a1 chore: fix many cosmetic linter warnings (#486)
* fix whitespace warnings

Signed-off-by: James Hillyerd <james@hillyerd.com>

* fix a number of typos

Signed-off-by: James Hillyerd <james@hillyerd.com>

* fix many cosmetic linter warnings

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-15 18:02:26 -08:00
dependabot[bot]
864a2ba2d2 build(deps): bump actions/setup-node from 3 to 4 (#483)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-15 16:53:53 -08:00
dependabot[bot]
b586ebe210 build(deps): bump actions/setup-go from 3 to 5 (#482)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-15 16:35:56 -08:00
dependabot[bot]
bed49706fc build(deps): bump golangci/golangci-lint-action from 3 to 4 (#481)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-15 16:28:23 -08:00
James Hillyerd
6ce1fd6347 actions: lint on push to any branch (#484)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-15 16:19:10 -08:00
dependabot[bot]
3c0f253820 build(deps): bump docker/setup-buildx-action from 2 to 3 (#480)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-15 15:46:03 -08:00
dependabot[bot]
b2a77ad522 build(deps): bump docker/login-action from 2 to 3 (#479)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-15 15:44:03 -08:00
James Hillyerd
32cc4fc56d actions: add dependabot config (#478)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-15 15:42:26 -08:00
corey-aloia
3112deb3e6 [Rest Client] changing to enable relative urls (#477)
* changing to enable relative urls

Signed-off-by: Corey Aloia <corey.aloia@sap.com>
2024-02-09 08:53:44 -08:00
James Hillyerd
975cb4ca5e CHANGELOG: tweak release process (#473)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-05 13:06:27 -08:00
James Hillyerd
97506b2d2b update changelog for 3.1.0-beta2 (#471)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-05 12:46:42 -08:00
James Hillyerd
8667c70257 yarn/js: flexible version specs (#470)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-05 10:46:03 -08:00
James Hillyerd
a27b57df67 yarn/js: update parcel & deps (#469)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-04 21:53:34 -08:00
James Hillyerd
d8746e8093 node: 20.x (#468)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-04 21:25:43 -08:00
James Hillyerd
599200d32e go: update dependencies (#467)
* go: update dependencies
* go: minimum 1.21 required by deps

Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-04 20:50:49 -08:00
James Hillyerd
e190adef4d go: 1.21 (#466)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-04 20:03:30 -08:00
James Hillyerd
6a389c78cc rest/client: fix comment lint error (#465)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2024-02-04 19:50:02 -08:00
James Hillyerd
6d66012a0c Rename client.WithOptTransport (#464)
* Update apiv1_client_opts.go
* Update apiv1_client_test.go
2024-01-25 10:06:35 -08:00
corey-aloia
b5ccd3da51 [Rest Client] Use options for client.New (#463)
* adding in clientopts to the rest client

Signed-off-by: Corey Aloia <corey.aloia@sap.com>
2024-01-25 09:46:31 -08:00
dependabot[bot]
2d409bb2b1 build(deps): bump msgpackr from 1.9.9 to 1.10.1 in /ui (#461)
Bumps [msgpackr](https://github.com/kriszyp/msgpackr) from 1.9.9 to 1.10.1.
- [Release notes](https://github.com/kriszyp/msgpackr/releases)
- [Commits](https://github.com/kriszyp/msgpackr/compare/v1.9.9...v1.10.1)

---
updated-dependencies:
- dependency-name: msgpackr
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-29 09:11:44 -08:00
James Hillyerd
4262b34a20 enmime: update to 1.1.0 (#459) 2023-12-15 17:45:24 -08:00
James Hillyerd
746f3bffbd actions: test build on windows (#458)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-12-13 17:02:24 -08:00
James Hillyerd
5a5864fde6 storage test: fix failures on windows (#457)
* storage test: close source reader
* fstore: properly close directory handle
2023-12-12 19:31:14 -08:00
James Hillyerd
e6e4e0987d storage: $ can be used in place of : in filestore path (#449) 2023-11-30 19:45:26 -08:00
James Hillyerd
f0473c5d65 docker: Give example of non-discard domain in greeting (#453)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-30 15:18:01 -08:00
James Hillyerd
288288ee85 git: do not treat golden files as text (#452) 2023-11-29 17:13:04 -08:00
James Hillyerd
32b83e6345 test: preserve SYSTEMROOT on windows (#451) 2023-11-29 16:36:40 -08:00
James Hillyerd
043551343c storage: fail startup if unable to create file store dir (#448)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-28 15:09:17 -08:00
James Hillyerd
c1d5d49126 event: Use pointers for InboundMessage addresses (#447)
* event: Use pointers for InboundMessage addresses

To ease conversions to/from MessageMetadata

Signed-off-by: James Hillyerd <james@hillyerd.com>

* message: test StoreManager.MailboxForAddress()

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-22 17:28:33 -08:00
James Hillyerd
d2121a52a9 message: Prefer To header for BeforeMessageStored event (#446)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-22 16:58:14 -08:00
James Hillyerd
3e94aacc20 message: improve manager test coverage (#438) 2023-11-21 15:40:14 -08:00
James Hillyerd
41889ee83a test: impl StoreStub.PurgeMessages (#444)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-21 14:56:21 -08:00
James Hillyerd
1b5a783dbd test: impl StoreStub.MarkSeen (#443)
* test: impl StoreStub.MarkSeen

Signed-off-by: James Hillyerd <james@hillyerd.com>

* continue to Message interface in StoreStub

Signed-off-by: James Hillyerd <james@hillyerd.com>

* test errors

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-18 11:21:09 -08:00
James Hillyerd
208d20582e test: Add tests for StoreStub (#440)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-17 16:34:49 -08:00
James Hillyerd
2a65c9beaa github actions: fix for #441, skip-pkg-cache: true (#442)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-17 16:00:01 -08:00
James Hillyerd
f411a42f90 github actions: work around golangci errors (#441)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-17 15:49:12 -08:00
James Hillyerd
13c39c8c0f github actions: test coverage on pushes to main (#439)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-17 10:45:55 -08:00
James Hillyerd
1088ccb8d1 luahost: add type check TODOs (#436)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-13 13:26:41 -08:00
James Hillyerd
d304cbd88b github actions: add missing Go linter (#428) 2023-11-13 12:30:32 -08:00
James Hillyerd
e56638cbac various: resolve linter errors in a number of pkgs (#434)
- webui: resolve linter errors
- msghub: resolve linter errors
- policy: resolve linter errors
- extension: resolve linter errors
2023-11-13 12:21:19 -08:00
James Hillyerd
843cb8a015 lifecycle: Don't create multiple notify channels (#435)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-13 11:54:14 -08:00
James Hillyerd
535438342e server: resolve linter errors (#433)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-13 11:34:05 -08:00
James Hillyerd
e22ed26633 storage: resolve linter errors (#432)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-13 11:22:49 -08:00
James Hillyerd
1ce1674861 client: resolve linter errors (#431)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-13 11:11:08 -08:00
James Hillyerd
7ae7d29741 rest: resolve linter errors (#430) 2023-11-12 19:32:43 -08:00
James Hillyerd
86d762ac88 stringutil: fix golint comment format error (#429)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-12 13:21:27 -08:00
James Hillyerd
85e1c2c7d7 message: Verify empty mailbox list does not error (#424)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-12 13:10:29 -08:00
James Hillyerd
3e06050771 docker-build action: fix syntax error, use env. for conditionals (#427)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-12 12:30:35 -08:00
James Hillyerd
72adb5561d docker-build action: only push to registry on main branch and v* tag pushes (#426) 2023-11-12 11:49:00 -08:00
Cyd
20ef8af047 Reject invalidomain with wildcards (#412)
Co-authored-by: Cyril DUPONT <cyd@9bis.com>
2023-11-12 09:42:20 -08:00
James Hillyerd
d7c538a210 doc: add INBUCKET_SMTP_REJECTORIGINDOMAINS docs (#423)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-10 13:13:21 -08:00
James Hillyerd
ebd4b9504b ui: status: Display reject-origin-domains config (#422)
For #380

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-09 15:13:05 -08:00
James Hillyerd
1d102a68b8 ui: update webcomponentsjs dep (#421)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-07 14:30:20 -08:00
James Hillyerd
11b581bbb5 ui: update parcel deps (#420)
* ui: update parcel dep

Signed-off-by: James Hillyerd <james@hillyerd.com>

* ui: update parcel-namer-rewrite dep

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-07 14:12:24 -08:00
James Hillyerd
d1e52ad971 ui: update elm dep (#419)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-07 13:43:26 -08:00
James Hillyerd
4a6b727cbc lua: bind BeforeMessageStored function (#418)
* lua: Restore missing test log output
* lua: Use logger for test assert_async output
* lua: add InboundMessage bindings
* lua: bind BeforeMessageStored function

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-06 18:10:02 -08:00
James Hillyerd
01fb161df8 extension: BeforeMessageStored event to rewrite envelope (#417)
* extension: add InboundMessage type
* manager: fires BeforeMessageStored event
* manager: Reacts to BeforeMessageStored event response
* manager: Apply BeforeMessageStored response fields to message

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-06 14:53:38 -08:00
James Hillyerd
0cb62af074 message: Add test for recipient policy (#416)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-03 12:34:58 -07:00
James Hillyerd
3731837127 docker: don't build until after PR submitted (#415) 2023-11-02 17:46:26 -07:00
James Hillyerd
74a27875e9 message: migrate more delivery logic into manager.go (#414)
* message: migrate more delivery logic into manager.go

Signed-off-by: James Hillyerd <james@hillyerd.com>

* manager: tidy up a few things

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-11-01 13:54:27 -07:00
James Hillyerd
b655c0cc11 smtp: Use enmime.DecodeHeaders for better performance (#413)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-10-30 17:10:09 -07:00
James Hillyerd
163a84f353 lua: fix incorrect function name (#409)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-10-23 17:05:56 -07:00
James Hillyerd
d6c23df241 lua: Expose logger object (#407)
Allows Lua scripts to add entries to Inbuckets log

Closes #327

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-10-23 16:06:19 -07:00
Saulius Gurklys
b1acff08a3 UTC format timestamps in headers (#404) (#406)
This change makes all header timestamps (e.g. "Received") to use
UTC time zone.

This way the length of such headers do not depend on local time zone
which if effect fixes #404.

Signed-off-by: Saulius Gurklys <s4uliu5@gmail.com>
2023-10-21 16:22:49 -07:00
dependabot[bot]
3d162549b1 build(deps): bump golang.org/x/net from 0.15.0 to 0.17.0 (#400)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.15.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.15.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-18 11:42:35 -07:00
James Hillyerd
d2fad433d7 Dockerfile: add missing node-gyp build dep to node:18 image (#403)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-10-16 16:00:35 -07:00
Benson Margulies
beb5abc62d Add a ForceTLS flag for SMTP. (#402)
When this is enabled, the server listens with TLS instead of waiting for
STARTTLS.

Signed-off-by: Benson Margulies <bimargulies@google.com>
2023-10-16 14:31:16 -07:00
James Hillyerd
3709aa8b51 message: Include inlines when returning attachments (#398)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-09-14 14:29:33 -07:00
James Hillyerd
63e47a4e74 web: Redirect base path prefix to prefix/ (#397)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-09-13 12:18:08 -07:00
James Hillyerd
5eb9592637 docker: fix insignificant ENV typo (#396)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-09-13 12:07:53 -07:00
James Hillyerd
9eabf94c48 goreleaser: remove deprecated rlcp option (#395)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-09-13 11:43:14 -07:00
James Hillyerd
0128be1f64 Fix Docker image link (#394)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-09-13 10:15:04 -07:00
James Hillyerd
d5553030d2 Fix service paths (#393)
* service: fix ExecStart path for pre-built packages

Signed-off-by: James Hillyerd <james@hillyerd.com>

* packaging: fix goreleaser and service paths

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-09-12 15:33:04 -07:00
James Hillyerd
f070347535 goreleaser: use default snapshot name template (#392)
Our manually configured template was breaking debian package snapshots,
version must start with a number
2023-09-12 14:33:15 -07:00
James Hillyerd
cafd2c3d66 Add names to better distinguish workflows (#391) 2023-09-11 13:47:53 -07:00
James Hillyerd
dcd60b47dd update go module dependencies (#390) 2023-09-11 13:28:25 -07:00
dependabot[bot]
6a30a294c6 build(deps): bump semver from 5.7.1 to 5.7.2 in /ui (#373)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-09 12:57:00 -07:00
James Hillyerd
9836c0ffbb go: update mod and imports to correctly reflect major version (#388) 2023-09-09 12:36:21 -07:00
James Hillyerd
558f3de083 nix shell: use go 1.20 (#387) 2023-09-09 11:48:53 -07:00
Benson Margulies
00736cc704 pop3 TLS: don't do server-side handshake explicitly... (#386) 2023-09-07 09:56:19 -07:00
Benson Margulies
9f0fef3180 Implement STLS for pop3 (#384) 2023-09-05 14:28:26 -07:00
Shantanu Gadgil
f1dadba1b2 inbucket cmd: add version flag (#385) 2023-08-31 10:00:41 -07:00
Cyd
06ec140e72 add reject from origin domain feature (#375)
Add a new feature to be able to reject email *from* specific domains.

Co-authored-by: Cyril DUPONT <cyd@9bis.com>
2023-08-26 11:05:20 -07:00
James Hillyerd
7c13a98ad2 ui: bump nodejs to 18 (LTS) (#381) 2023-08-25 12:11:45 -07:00
guangwu
0ae452ed17 chore: remove refs to deprecated io/ioutil (#376)
Signed-off-by: guoguangwu <guoguangwu@magic-shield.com>
2023-08-07 16:25:36 -07:00
Carlos Tadeu Panato Junior
926f9f3804 Few updates in ci jobs and go.mod/dockerfile (#372)
* update go.mod to 1.20
* clean up and format ci jobs
* update go and alpine images in dockerfile

Signed-off-by: cpanato <ctadeu@gmail.com>
2023-05-30 14:08:07 -07:00
James Hillyerd
87888e9dbf Update changelog for 3.1.0 beta1 (#353)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-28 11:00:39 -08:00
James Hillyerd
e84d21cb28 events: Remove unnecessary go calls (#352) 2023-02-28 10:25:16 -08:00
James Hillyerd
5a886813c3 Provide inbucket object in Lua (#351)
* fix delve fortify thingy
* Expose inbucket.after.message_stored in lua
* Expose inbucket.after.message_deleted in lua
* Expose inbucket.before.mail_accepted in lua
2023-02-27 20:22:10 -08:00
James Hillyerd
95281566f6 bump Go deps (#350)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-27 10:59:53 -08:00
James Hillyerd
7044567d64 msghub: Clear deleted messages instead of unlinking (#348)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-17 15:06:43 -08:00
James Hillyerd
82ddf2141c Create V2 API for monitor+deletes, revert V1 API (#347)
* Revert socketv1 controller API to maintain V1 contract, introduce
V2 controller for Inbucket UI.

Signed-off-by: James Hillyerd <james@hillyerd.com>

* Introduce MessageID for deletes, instead of recycling header

Signed-off-by: James Hillyerd <james@hillyerd.com>

* Update UI for monitor V2 API

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-17 12:37:17 -08:00
James Hillyerd
b554c7db83 Fix doc name for LUA config param (#339)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-17 12:35:08 -08:00
James Hillyerd
36095a2cdf extension: split out an async specific broker for "after" events (#346)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-16 16:17:06 -08:00
James Hillyerd
e1b8996412 goreleaser: set archive rlcp true to mirror upcoming default (#344)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-16 11:25:28 -08:00
James Hillyerd
71d3e8df3b goreleaser: correct archive naming by using default (#343)
.Binary was causing some archives to be called inbucket_client

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-16 10:59:57 -08:00
James Hillyerd
2da7ad61cd ui: update browser list (#342) 2023-02-16 09:59:26 -08:00
James Hillyerd
eaae1a1e44 GHA: Use node16 actions, Go 1.20 (#340)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-15 21:01:24 -08:00
James Hillyerd
a55da8b7d1 file & mem stores should emit delete events on purge (#338)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-14 19:51:42 -08:00
James Hillyerd
561ed93451 feat: Monitor tab dynamically updates when messages are deleted (#337)
* WIP: msghub handles deletes, UI does not yet display them

Signed-off-by: James Hillyerd <james@hillyerd.com>

* socket and UI support message deletes

Signed-off-by: James Hillyerd <james@hillyerd.com>

* use Delete naming for consistency

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-14 19:02:06 -08:00
James Hillyerd
ef12d02b83 msghub: Recover and log panics (#336)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-14 14:00:13 -08:00
James Hillyerd
de617b6a73 delete event: test lua func (#335)
* lua: tidy test helpers

Signed-off-by: James Hillyerd <james@hillyerd.com>

* lua: test after_message_deleted func

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-14 10:56:43 -08:00
James Hillyerd
69b6225554 storage: emit AfterMessageDeleted events (#334)
* Ignore test lua script

Signed-off-by: James Hillyerd <james@hillyerd.com>

* Wire ExtHost into storage system
imports

Signed-off-by: James Hillyerd <james@hillyerd.com>

* storage/file: emit deleted events

Signed-off-by: James Hillyerd <james@hillyerd.com>

* storage/mem: emit deleted events

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-13 17:11:04 -08:00
James Hillyerd
5adef42df7 Replace message.Metadata usage with event.MessageMetadata (#333)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-13 13:52:28 -08:00
James Hillyerd
d11ae3710c config: make note of domain addressing (#331) 2023-02-12 16:17:41 -08:00
James Hillyerd
5d18d79539 lua: Preload json module (#330)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-10 11:44:36 -08:00
James Hillyerd
b38b2e9760 lua: Preload gluahttp module (#328) 2023-02-09 19:04:33 -08:00
James Hillyerd
75b7c69b5c lua: Add getter/setter tests for bound objects (#326)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-08 17:20:57 -08:00
James Hillyerd
239426692e lua: Use table syntax for user object bindings (#325)
* lua: update bind_message to use table syntax

* lua: update bind_address to use table syntax
2023-02-08 13:38:00 -08:00
James Hillyerd
17b054b5a1 add: direnv config (#324)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-02-08 10:32:19 -08:00
James Hillyerd
7f91c3e9cb lua: Bind after_message_stored and before_mail_accepted (#322)
Signed-off-by: James Hillyerd <james@hillyerd.com>

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-01-24 16:37:26 -08:00
James Hillyerd
55addbb556 lua: Init with config and pool (#321)
* lua: Intial impl with config and pool

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-01-24 12:16:58 -08:00
James Hillyerd
8fd5cdfc86 extension: Add BeforeMailAccepted event (#320)
Signed-off-by: James Hillyerd <james@hillyerd.com>

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-01-22 18:01:56 -08:00
James Hillyerd
e74efbaa77 extension: Make AfterMessageStored async (#319) 2023-01-22 16:26:52 -08:00
James Hillyerd
b383fbf9ab manager: Test that MessageStored event is emitted (#318)
Signed-off-by: James Hillyerd <james@hillyerd.com>

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-01-17 19:26:44 -08:00
James Hillyerd
c9912bc2bb Fix incorrect test capitalization (#317)
Signed-off-by: James Hillyerd <james@hillyerd.com>

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-01-17 19:08:59 -08:00
James Hillyerd
f0d457b8f5 extension: Add MessageStored event (#316)
* Replace existing direct StoreManager->msghub communication with this
  event
* For #280 #309 #312 #310

Signed-off-by: James Hillyerd <james@hillyerd.com>

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-01-16 21:30:47 -08:00
James Hillyerd
3bf4b5c39b extension: Implement an EventBroker (#315)
Signed-off-by: James Hillyerd <james@hillyerd.com>

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-01-16 20:34:16 -08:00
James Hillyerd
37806f222d shell.nix: add a quick-test script, qt (#314) 2023-01-16 20:15:40 -08:00
James Hillyerd
f5899c293c etc: scripts use /usr/bin/env bash (#313)
Signed-off-by: James Hillyerd <james@hillyerd.com>

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-01-16 16:10:27 -08:00
James Hillyerd
cd9c3d61ee shell.nix: Add gopls as build dep (#308)
Signed-off-by: James Hillyerd <james@hillyerd.com>

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-01-16 14:29:38 -08:00
James Hillyerd
37d314fd2e Makefile: fix, wasn't testing SRC timestamps (#307)
Signed-off-by: James Hillyerd <james@hillyerd.com>

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-01-15 16:41:11 -08:00
James Hillyerd
28b0557865 Update go.mod to 1.18 syntax (#306)
Signed-off-by: James Hillyerd <james@hillyerd.com>

Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-01-13 09:09:28 -08:00
James Hillyerd
997cb55847 Update go deps (#304)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2023-01-12 20:28:24 -08:00
James Hillyerd
61454a0c9c Build with Go 1.19 (#305)
Also adds `:edge` tag to dev docker-run.sh script
2023-01-12 20:19:40 -08:00
dependabot[bot]
e875a4c382 build(deps): bump json5 from 2.2.1 to 2.2.3 in /ui (#302)
Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.3)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-08 10:09:25 -08:00
James Hillyerd
bc6548b6f3 Update CHANGELOG for 3.0.4 2022-10-02 15:35:59 -07:00
James Hillyerd
911a6c8d78 bump nix go to 1.18 2022-10-02 13:14:01 -07:00
James Hillyerd
547a12ffca smtp: Adjust fromRegex to handle AUTH=<> in middle of args (#291)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2022-09-03 14:03:55 -07:00
James Hillyerd
c8d22ac802 smtp: Break up handler tests (#290)
* Do drain logging from main to reduce test output
* Break up some of the larger handler test funcs
* Introduce sub-tests
2022-08-25 14:19:57 -07:00
James Hillyerd
9dbffa88de Refactor SMTP handler_test log collection (#289)
* smtp: allow logger to be passed into startSession
* smtp: remove logbuf and teardown from handler_test
* smtp: handler_test log output to testing.T
2022-08-24 21:42:53 -07:00
James Hillyerd
eae4926b23 Dependency injection improvements (#288)
Refactor server life-cycle into it's own file, make service startup and monitoring more consistent and testable.

* Extract services creation in preparation for DI
* pop3: rename New to NewServer
* lifecycle: Add fatal error Notify()
* web: Introduce Server struct w/ Notify()
* Extract Start in lifecycle
* Add Start() to Hub
* RetentionScanner startup consistent with other svcs
* Remove global shutdown channel
* Implement a readiness notification system
2022-08-13 13:22:34 -07:00
Abirdcfly
29d1ed1e7f delete minor unreachable code caused by t.Fatal (#287)
Signed-off-by: Abirdcfly <fp544037857@gmail.com>
2022-08-10 08:59:09 -07:00
James Hillyerd
1f1a8b4192 Handle IP address domains (#285)
* Add basic TestRecipientAddress tests

* Handle forward-path route spec

* Validate IP addr "domains"

* Forward-path test cases

* Add integration test

* Add IPv4 recip swaks test

* Special case domain mailbox extraction

* add IPv6 swaks test

* Formatting

Signed-off-by: James Hillyerd <james@hillyerd.com>

* Update changelog
2022-08-07 20:13:58 -07:00
James Hillyerd
344c3ffb21 Update CHANGELOG for 3.0.3 (#286)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2022-08-07 19:39:48 -07:00
James Hillyerd
87018ed42d Allow AUTH=<> FROM parameter (#284)
* Use backtick on regex
* Accept AUTH=<> FROM parameter
* Update changelog
2022-07-30 10:57:29 -07:00
James Hillyerd
1650a5b375 Merge pull request #281 from inbucket/dependabot/npm_and_yarn/ui/terser-5.14.2
build(deps): bump terser from 5.12.1 to 5.14.2 in /ui
2022-07-30 09:41:41 -07:00
dependabot[bot]
3f7adbfb22 build(deps): bump terser from 5.12.1 to 5.14.2 in /ui
Bumps [terser](https://github.com/terser/terser) from 5.12.1 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-21 02:45:45 +00:00
James Hillyerd
03cc31fb70 Build with Go 1.18 2022-07-04 16:23:06 -07:00
James Hillyerd
a10a6244c9 Merge pull request #279 from inbucket/v302rel
Release 3.0.2
2022-07-04 12:02:17 -07:00
James Hillyerd
9185423022 Update CHANGELOG 2022-07-04 11:03:48 -07:00
James Hillyerd
9aaca449f8 Fix non-root basepaths, closes #273 2022-05-22 21:47:52 -07:00
James Hillyerd
f39395bd7f Fix error in swaks tests server default env var 2022-05-22 21:47:52 -07:00
James Hillyerd
2c68128d5d swaks-tests: allow server address override 2022-05-08 13:13:25 -07:00
James Hillyerd
06d4120682 Migrate to Yarn & Parcel (#260)
* Switch from npm to yarn
* Add minimum viable parcel dev server config
* Remove webpack configs
* Update docker build, build w/ yarn on node 16.x
2022-04-23 13:35:54 -07:00
James Hillyerd
58bcd4f557 Docker frontend build runs as amd64 (#270) 2022-04-23 10:33:46 -07:00
kaustubh105
e91e8d5aee Add support for ARMv7 and ARM64 docker images (#267)
* Build armv7 and arm64 docker containers

* Add QEMU step to build for multi arch

* Pin qemu action version
2022-02-25 08:56:18 -08:00
James Hillyerd
5322462899 Update changelog for 3.0.1-rc2
Signed-off-by: James Hillyerd <james@hillyerd.com>
2022-01-23 17:42:32 -08:00
Daniel Simkus
5def9ed183 Add arm and arm64 builds (#256)
* Add arm and arm64 builds

Signed-off-by: danielsimkus <daniel.simkus@carandclassic.com>

* Added arm and arm64 to client, and switched to goarm 7

Signed-off-by: danielsimkus <daniel.simkus@carandclassic.com>
2022-01-22 09:50:37 -08:00
James Hillyerd
357589d90e Rename master branch to main (#255)
* Update contributing guide, remove git-flow references

Signed-off-by: James Hillyerd <james@hillyerd.com>

* Update changelog for main branch rename

Signed-off-by: James Hillyerd <james@hillyerd.com>

* Update github actions for branch rename

* Update README build badges

Signed-off-by: James Hillyerd <james@hillyerd.com>

* Update README for new branch names

Signed-off-by: James Hillyerd <james@hillyerd.com>

* Note branch rename in change log

Signed-off-by: James Hillyerd <james@hillyerd.com>
2022-01-17 17:16:44 -08:00
James Hillyerd
b664bcfc4c Update changelog for 3.0.1-rc1 2022-01-17 13:28:00 -08:00
James Hillyerd
ffd13e2ee7 Update go deps (#253)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2022-01-17 13:17:50 -08:00
James Hillyerd
747775b8f2 Update npm deps (#252)
Signed-off-by: James Hillyerd <james@hillyerd.com>
2022-01-17 12:48:44 -08:00
dependabot[bot]
2c0d942c76 build(deps): bump follow-redirects from 1.14.0 to 1.14.7 in /ui (#247)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.0 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.0...v1.14.7)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-17 12:24:06 -08:00
James Hillyerd
e7263439d5 Correct goreleaser config (#250)
* Correct glob patterns to include missing index.html
* Remove broken homebrew config
2022-01-17 12:19:37 -08:00
James Hillyerd
cb6f99c487 Merge branch 'release/3.0.0' 2021-10-02 10:52:59 -07:00
James Hillyerd
04fb58e15e Update CHANGELOG for final 3.0.0 2021-09-19 11:34:43 -07:00
James Hillyerd
f11ad55474 Merge tag 'v3.0.0-rc4' into develop
v3.0.0-rc4

Fixed
- Various MIME header decoding improvements

Changed
- Bump Go version to 1.17 (#233)
2021-08-22 13:37:26 -07:00
James Hillyerd
26939f2bf6 Merge branch 'release/3.0.0-rc4' 2021-08-22 13:36:32 -07:00
James Hillyerd
05a3b1742a Update CHANGELOG for rc4 2021-08-22 13:32:47 -07:00
James Hillyerd
867d5f5d7f Update npm deps (#235) 2021-08-22 13:25:42 -07:00
James Hillyerd
8e34a21dc6 Update Go dependencies, incl enmime (#234) 2021-08-22 12:54:26 -07:00
James Hillyerd
8869acef0b Bump Go to 1.17 (#233)
* Bump Go to 1.17

* update chglog
2021-08-22 12:31:13 -07:00
James Hillyerd
752d5c9668 docker: tag versions with latest (#232) 2021-08-07 10:56:40 -07:00
James Hillyerd
091e26c467 docker-build should also run on tags (#230) 2021-08-01 13:19:19 -07:00
James Hillyerd
6593a36b48 Merge tag 'v3.0.0-rc3' into develop
Release is for CI/CD changes only.
2021-08-01 13:02:26 -07:00
James Hillyerd
68ef2d9873 Merge branch 'release/3.0.0-rc3' 2021-08-01 13:01:35 -07:00
James Hillyerd
ab988caf6b Add rc3 to change log 2021-08-01 13:00:28 -07:00
James Hillyerd
fa62220d98 Add ghcr.io to docker metadata images 2021-08-01 12:29:26 -07:00
James Hillyerd
1ecf424975 login to GitHub container registry 2021-08-01 12:14:28 -07:00
James Hillyerd
3342938dd4 Update to docker-push-action v2 w/ metadata, buildx (#228)
Docker Hub no longer builds for us, so we need to switch to GHA
2021-08-01 11:57:46 -07:00
James Hillyerd
6be1655723 Update to docker-push-action v2 w/ metadata, buildx (#228)
Docker Hub no longer builds for us, so we need to switch to GHA
2021-08-01 11:52:17 -07:00
James Hillyerd
1465e6fb49 Merge tag 'v3.0.0-rc2' into develop
Added:

- Support for SMTP AUTH (#197, thanks makarchuk)
- Dark mode support (#218, thanks nerones)

Fixed:

- Prevent potential click jacking (#190, thanks stuartskelton)
- Error on 8 character long SMTP commands (#221)
- Allow empty username and password during AUTH (#225)
2021-07-31 16:41:35 -07:00
James Hillyerd
21991cbfc7 Merge branch 'release/3.0.0-rc2' 2021-07-31 16:40:17 -07:00
James Hillyerd
7138a97935 Update change log for 3.0.0-rc2 2021-07-31 16:32:01 -07:00
James Hillyerd
beee68fc5d Update change log 2021-07-31 16:26:47 -07:00
James Hillyerd
9e2af71743 Upgrade node deps (#227)
* bump node deps

* npm audit fix
2021-07-31 16:09:30 -07:00
James Hillyerd
a2c4292fc1 update go deps (#226) 2021-07-31 15:53:38 -07:00
James Hillyerd
2016142747 smtp: allow empty user & pass during AUTH LOGIN (#225) 2021-07-31 10:38:48 -07:00
James Hillyerd
4f9f961cac smtp: fix formatting (#224) 2021-07-31 10:32:08 -07:00
Nelson Efrain A. Cruz
bf8536abb3 Adds dark mode support (#218)
* Adds dark mode support

Updates the css to support dark mode via media query.
The dark theme its heavily inspired on the new dark mode for google.com.
2021-07-20 08:07:06 -07:00
James Hillyerd
985f2702f2 Fix command line length bug (#221)
* handler: Don't fail on 8 character command lines

Fixes #214

* handler: Test that STARTTLS is parsed correctly.
2021-07-11 12:00:28 -07:00
James Hillyerd
11f3879442 goreleaser: update nfpm config to use contents attrib (#220)
fixes #219

Signed-off-by: James Hillyerd <james@hillyerd.com>
2021-07-11 10:05:00 -07:00
James Hillyerd
8562c55c98 nix: add elm-json for updating pkgs 2021-05-06 12:27:01 -07:00
James Hillyerd
e3066bb535 Update nodejs dependencies (#209)
* node: Update top level deps

* node: Audit fix
2021-05-06 11:22:50 -07:00
James Hillyerd
35ab31efbc Update go deps (#208) 2021-05-06 10:35:53 -07:00
James Hillyerd
81edf40996 store_test: Fix t.Fatal non-test goroutine lint error 2021-05-06 09:58:30 -07:00
James Hillyerd
c64e7a6a6c Revert "Add support for AUTH, closes #62" (#206)
This reverts commit 261bbef426.  It was merged to directly master by mistake, and should have gone through the normal release process.
2021-05-03 20:56:10 -07:00
James Hillyerd
4bd64563f2 Bump nodejs to 14.x (#203) 2021-05-01 16:50:43 -07:00
James Hillyerd
66dec49a49 Bump Go version to 1.16 (#202)
* bump go version
* Docker: bump go/alpine version
2021-05-01 14:16:59 -07:00
James Hillyerd
649e3743e0 Migrate off Travis CI (#201)
Adds generic Go matrix build with coverage, removes Travis config.
2021-05-01 13:49:41 -07:00
Timur Makarchuk
c096f018d6 Add support for AUTH, closes #62
* Add PLAIN and LOGIN auth support
2021-04-10 13:58:18 -07:00
Timur Makarchuk
261bbef426 Add support for AUTH, closes #62
* Add PLAIN and LOGIN auth support
2021-04-10 13:45:09 -07:00
Stuart Skelton
3c5960aba0 Avoid potential click jacking (#190) 2020-11-19 08:16:01 -08:00
James Hillyerd
7f430f2bde Merge tag 'v3.0.0-rc1' into develop
Added:

- Refresh button to reload mailbox contents
- Improved keyboard (tab) focus highlights

Changed:

- The UI now includes the Open Sans webfont instead of relying on browser/OS
  fonts
2020-09-24 20:59:30 -07:00
James Hillyerd
c480fcb341 Merge branch 'release/3.0.0-rc1' 2020-09-24 20:58:53 -07:00
James Hillyerd
e74f5e5116 Update changelog for 3.0.0-rc1 2020-09-24 20:54:07 -07:00
James Hillyerd
6ce045ddb7 Update changelog 2020-09-24 20:43:56 -07:00
James Hillyerd
9b03c311db ui: Replace Mailbox Session use with ServeUrl (#185)
Plus a couple UI padding tweaks
2020-09-24 15:59:12 -07:00
James Hillyerd
ebd25a60e1 ui: Remove to do comments, must keep session.router 2020-09-24 11:19:04 -07:00
James Hillyerd
7c87649579 ui: Convert Layout to use Effects 2020-09-23 23:00:29 -07:00
James Hillyerd
e56365b9a0 Update deps (#184)
* backend: update Go dependencies
* frontend: update npm dependencies
2020-09-23 21:39:27 -07:00
James Hillyerd
698b0406c8 Readme updates (#183)
* Add docker build badge
* Rephrase things
* add dev guide link
* Remove brew tap section until #68 is fixed
2020-09-23 20:43:51 -07:00
James Hillyerd
361bbec293 ui: Keyboard accessibility focus highlights (#180)
* Focus indication for mailbox message list
* Add focus for monitor message list
2020-09-22 14:11:06 -07:00
James Hillyerd
407ae87a3b ui: Add refresh button to mailbox page (#179)
`socketConnected` is not implemented, but will be used when we implement #92
2020-09-21 20:11:32 -07:00
James Hillyerd
4648d8e593 ui: Use OpenSans font (#178) 2020-09-21 16:16:00 -07:00
James Hillyerd
5c5b0f819b Effects refactor continued (#177)
* Use Effects instead of replaceUrl in Mailbox
* Add Effect.navigateRoute to handle monitor message clicks
* Add a focusModal effect for mailbox purge
* Remove temporary Cmd wrapper Effect
2020-09-13 17:08:11 -07:00
James Hillyerd
8adfd82232 ui: add npm run clean script 2020-09-13 10:39:16 -07:00
James Hillyerd
2162a4caaa ui: Add an Effect system to handle global state and Elm Cmds (#176)
All pages now leverage Effects for most of their Session and Cmd requests. More work required for routing and other lingering Cmd use.
2020-09-12 19:45:14 -07:00
James Hillyerd
cf4c5a29bb ui: Force file watch on dev server
file watch stopped working
2020-09-12 16:32:40 -07:00
James Hillyerd
6598b09114 ui: Start dev server with default host, not 0.0.0.0
0.0.0.0 does not work well with WSL2 on Windows 10
2020-09-06 16:38:33 -07:00
James Hillyerd
ce5bfddaa5 Migrate release process from travis to github (#175)
* set fetch depth to 0
* Only snapshot when not tagged
* Run deploy for v* tags
* travis: remove deploy stage
2020-09-05 14:21:42 -07:00
James Hillyerd
2934d799ef Add a GitHub workflow for building a snapshot release 2020-09-04 15:06:57 -07:00
James Hillyerd
8a07a24828 Merge tag 'v3.0.0-beta3' into develop 2020-09-04 12:45:45 -07:00
James Hillyerd
2408ace6c2 Merge branch 'release/3.0.0-beta3' 2020-09-04 12:45:14 -07:00
James Hillyerd
1a5db5b5f8 update CHANGELOG 2020-09-04 12:44:50 -07:00
James Hillyerd
f712f5b0f3 Update frontend dependencies (#174)
* ui: update top level npm deps

* ui: update webpack dependencies

* ui: update elm-webpack-loader

* ui: npm audit fix
2020-08-30 18:48:41 -07:00
James Hillyerd
f0520b88c5 Update backend and docker dependencies (#173)
* backend: update dependencies
* travis: go 1.15
* docker: bump to go 1.15, alpine 3.12
2020-08-30 13:18:58 -07:00
James Hillyerd
5a0c4778cb Set base path in index.html (#172)
- Create a new index-dev.html for webpack live server
- Update Go+index.html to set <base href>
- Fixes #171
2020-08-29 19:06:21 -07:00
James Hillyerd
289b38f016 Add configurable base path for reverse proxy use (#169)
* ui: Refactor routing functions into Router record
* ui: Store base URI in AppConfig
* ui: Use basePath in Router functions
* backend: Add Web.BasePath config option and update routes
* Tweaks to get SPA to bootstrap basePath configured
* ui: basePath support for apis/serve
* ui: basePath support for message monitor
* web: Redirect requests to / when basePath configured
* doc: add basepath to config.md
* Closes #107
2020-08-09 15:53:15 -07:00
James Hillyerd
316a732e7f cmd, pkg: add line breaks to several go source files 2020-07-26 11:59:27 -07:00
Sascha Andres
f0bc5741f3 feat: update gorilla websocket (#167)
Closes #144
2020-07-25 10:34:15 -07:00
Sascha Andres
046de42774 allow empty envelope (#166)
* feat: allow empty MAIL FROM

Closes #164
2020-07-25 10:23:31 -07:00
James Hillyerd
860045715c docker-run.sh: Update image repo 2020-06-28 11:02:49 -07:00
James Hillyerd
001e9fec58 Rollback #153 as it breaks storage volumes, closes #27 (#161)
Keeping tzdata pkg
2020-06-28 11:00:51 -07:00
James Hillyerd
2e0b7cc097 Merge docker github action for #160 2020-06-28 09:31:07 -07:00
James Hillyerd
b0bbf2e9f5 github: Add action to test build docker image 2020-06-28 09:27:45 -07:00
Martijn Suijlen
3372ade61b Docker image should run non-root (#153)
Changed the Dockerfile so that there is a Inbucket user (and group). This will allow the container to be executed a the Inbucket user in stead of ROOT (security best practices)

If the user wants to use a different greeting.html file he can use the environment variable to define a different one. For now we just use the greeting.html from the defaults directory.

* Permissions for /start-inbucket.sh file
* Added timezone data so you can set the timezone in the image
* Updated Docker greeting.html file to include some basic instructions
* Updated to alpine 3.11
* Updated to golang 1.14
* Updated the required packages
2020-06-26 08:38:27 -07:00
James Hillyerd
62dd540be5 Merge branch 'feature/mailbox-timer' into develop 2020-04-12 16:29:31 -07:00
James Hillyerd
65a6ab2b4f ui: Simplify updateMarkMessageSeen 2020-04-11 16:11:28 -07:00
James Hillyerd
9e1da20782 ui: Update Mailbox to use Timer module 2020-04-11 15:32:35 -07:00
James Hillyerd
930801f6da Merge branch 'feature/mouse-out-delay' into develop 2020-04-05 17:47:57 -07:00
James Hillyerd
4fc8d229eb ui: impl Timer.schedule function 2020-04-05 17:27:22 -07:00
James Hillyerd
e8e506f870 ui: Refactor Timer into it's own module. 2020-04-05 16:22:16 -07:00
James Hillyerd
8a3d291ff3 ui: Improve layout menu function
- Rename menu to mainMenu for clarity
- Rename recent to recentMenu
- Add a mouseOut timer to recentMenu
2020-04-05 15:30:08 -07:00
James Hillyerd
107b649738 docker: Capture stderr in healthcheck 2020-03-29 20:49:38 -07:00
Martijn Suijlen
c91a3ecd41 docker: Add a healthcheck (#152)
Use the INBUCKET_WEB_ADDR value to get the port number of Inbucket using 'cut'. If the variable is not set, use the default value (and that is 0.0.0.0:9000). Healtcheck will check the exit code of the command executed (0=HEALTHY)
2020-03-29 20:40:39 -07:00
James Hillyerd
2c74268014 docker: Fix + some tweaks, fixes #155
- Reorder deps & builds
- Download elm binary
- g++ and python3 for node-gyp
- Bump go & alpine versions
- Use npm ci
2020-03-29 20:15:33 -07:00
James Hillyerd
da63e4d77a travis: s/-/_/ for elm deps 2020-03-29 19:32:41 -07:00
James Hillyerd
4a90b37815 ui: Implement modal focus trap 2020-03-29 19:26:59 -07:00
James Hillyerd
cabbdacb89 Merge branch 'feature/linter-fixes' into develop 2020-03-29 12:55:44 -07:00
James Hillyerd
baad19e838 ui: Add function signature to accept and store policies 2020-03-29 12:21:16 -07:00
James Hillyerd
c520af4983 ui: Linter dead code elimination 2020-03-29 11:54:12 -07:00
James Hillyerd
c312909112 ui: Cons related linter fixes 2020-03-29 11:41:23 -07:00
James Hillyerd
083b65c9bc ui: Ignore elm make generated index.html 2020-03-29 11:39:10 -07:00
James Hillyerd
59ae2112f7 ui: Import fixes for src directory 2020-03-29 11:35:22 -07:00
James Hillyerd
1a45179e31 ui: Linter import fixes for Page directory 2020-03-29 11:28:47 -07:00
James Hillyerd
2b857245f7 ui: Fix linter warnings in Data dir 2020-03-29 11:13:31 -07:00
James Hillyerd
9573504725 Merge branch 'feature/padding' into develop 2020-03-22 22:10:54 -07:00
James Hillyerd
c21066752f travis: Switch to ubuntu bionic w/ preinstalled nodejs 2020-03-22 21:53:19 -07:00
James Hillyerd
66c95baf05 ui: Horizontally center content on wide displays 2020-03-22 21:20:38 -07:00
James Hillyerd
22a7789b7b ui: Prevent mailbox dropdown overflow 2020-03-22 20:40:02 -07:00
James Hillyerd
d2da53cc0f ui: Convert main.css to mobile first 2020-03-22 18:49:45 -07:00
James Hillyerd
bfac9a0cc2 nix: add elm lang server, for nixos newer than 19.09 2020-03-22 17:48:02 -07:00
James Hillyerd
a64429ae61 Merge branch 'feature/elm-0191' into develop 2020-03-21 13:50:56 -07:00
James Hillyerd
2436f2e3de ui: bump elm filesize, date-format versions 2020-03-21 13:41:37 -07:00
James Hillyerd
fc76ce74cb ui: bump elm indirect versions 2020-03-21 13:36:15 -07:00
James Hillyerd
eef4bbdb01 ui: bump elm core libs 2020-03-21 13:29:25 -07:00
James Hillyerd
201987f6a8 ui: Upgrade to elm 0.19.1, bump all JS deps 2020-03-21 13:07:59 -07:00
James Hillyerd
45d9d2af39 travis: Update to Go 1.14.x 2020-03-21 10:04:24 -07:00
Fred Cox
12802e93cb Fix var name for tls cert (#146) 2019-09-17 08:51:07 -07:00
James Hillyerd
0956a13618 release: Modernize goreleaser config 2019-08-17 16:47:02 -07:00
James Hillyerd
de4bb991dd Merge tag 'v3.0.0-beta2' into develop
Added
- Ability to name mailboxes after domain of email recipient, set via
  `INBUCKET_MAILBOXNAMING`, thanks MatthewJohn.

Changed
- Updated JavaScript dependencies.
- Updated Go dependencies.
- Updated Docker build: Go to 1.12, and Alpine Linux to 3.10

Fixed
- URLs to view/download attachments from REST API, #138
- Support for late EHLO, #141
2019-08-17 16:11:41 -07:00
James Hillyerd
14f0895ae7 Merge branch 'release/3.0.0-beta2' 2019-08-17 16:11:12 -07:00
James Hillyerd
8bb01570ef Update changelog 2019-08-17 16:11:02 -07:00
James Hillyerd
3a1c757d04 smtp: Handle late EHLO, fixes #141 2019-08-17 15:46:28 -07:00
James Hillyerd
d8474d56e5 Merge branch 'feature/readme' into develop 2019-08-17 15:31:16 -07:00
James Hillyerd
eef45a4473 readme: Add Docker info 2019-08-17 15:29:05 -07:00
James Hillyerd
91d19308fe readme: Update build instructions, closes #127 2019-08-17 15:20:40 -07:00
James Hillyerd
e359c0b030 docker: go 1.11 -> 1.12, alpine linux 3.8 -> 3.10 2019-08-17 13:00:01 -07:00
James Hillyerd
a73ffeabd3 rest: Update attachments to 3.0 URL scheme, closes #138 2019-08-17 12:07:20 -07:00
James Hillyerd
0b3f4eab75 backend: Update dependencies 2019-08-10 17:54:09 -07:00
James Hillyerd
7ea4798e77 backend: Update to enmime 0.6.0 2019-08-10 17:13:57 -07:00
James Hillyerd
070de88bba ui: JS dependency updates 2019-08-10 16:28:48 -07:00
James Hillyerd
383386d5fb Update lodash 2019-08-10 16:00:04 -07:00
James Hillyerd
a3e2c5247e Update elm + elm-webpack-loader 2019-08-10 15:56:46 -07:00
James Hillyerd
702f9ef48e Update top-level JS deps 2019-08-10 14:48:54 -07:00
James Hillyerd
ac4501ba35 Update change log 2019-08-10 14:31:21 -07:00
Matt John
c78656b400 Add ability to name mailboxes after domain of email (#140)
* Add ability to name mailboxes after domain of email
* Update argument documentation for 'domain' filtering
* Update address policy to verify domain against RFC and return error on invalid domain
2019-08-10 14:13:21 -07:00
James Hillyerd
b6a6cc6708 Merge tag 'v3.0.0-beta1' into develop
Release 3.0.0-beta1
2019-03-14 21:29:19 -07:00
James Hillyerd
a17fa256a2 Merge branch 'release/3.0.0-beta1' 2019-03-14 21:26:15 -07:00
James Hillyerd
7ea8e2fc03 Release 3.0.0-beta1 2019-03-14 21:15:09 -07:00
James Hillyerd
a0b6f0692d Merge branch 'feature/gh-org' into develop for #126 2019-02-24 10:36:31 -08:00
James Hillyerd
c1b7e3605c goreleaser: update github owner to inbucket 2019-02-24 10:35:38 -08:00
James Hillyerd
2b3dd51e71 docs: rename to github.com/inbucket/inbucket 2019-02-24 10:09:33 -08:00
James Hillyerd
e4c48a0705 go code: rename to github.com/inbucket/inbucket 2019-02-24 09:43:10 -08:00
James Hillyerd
5c885a067a ui: Update npm deps 2019-02-24 09:02:52 -08:00
James Hillyerd
71b3de59af ui: Add search clear button 2019-02-24 08:42:33 -08:00
James Hillyerd
fc95f6e57f ui: Add polyfill for webcomponents 2019-02-18 19:28:44 -08:00
James Hillyerd
e5e1c39097 ui: Upgrade to NodeJS 10.x 2019-02-18 17:01:18 -08:00
James Hillyerd
f1b85be23a ui: Have dev-server bind to 0.0.0.0 2019-02-18 16:31:54 -08:00
James Hillyerd
aaf8eb5ec1 ui: Combine margin/padding 2019-02-18 15:48:08 -08:00
James Hillyerd
18b85877ab ui: Ignore submit of empty mailbox input 2019-02-18 13:04:21 -08:00
James Hillyerd
cd89d77d9f ui: Unknown routes redirect to Home 2019-02-18 12:51:49 -08:00
James Hillyerd
a54e0f2438 ui: Clear flash when changing routes 2019-02-18 12:35:27 -08:00
James Hillyerd
3738ccc11d ui: Remove unnecessary Session params from Mailbox 2019-02-18 11:09:36 -08:00
James Hillyerd
a6cdd30fb1 Merge branch 'feature/keybd-nav' into develop 2019-02-17 14:05:19 -08:00
James Hillyerd
a467829103 ui: Convert hamburger to button 2019-02-17 13:40:56 -08:00
James Hillyerd
b2255fefab ui: Allow keyboard navigation of message list 2019-02-17 12:59:05 -08:00
James Hillyerd
34799b9a04 ui: Move ClearFlash & ViewMailbox into Layout 2019-02-17 12:21:20 -08:00
James Hillyerd
cfbd30d8b0 ui: Give Layout it's own Model/Msg 2019-02-17 11:22:59 -08:00
James Hillyerd
7cd45ff3c7 ui: Allow tab+enter nav of monitor messages 2019-02-17 08:55:59 -08:00
James Hillyerd
3c2b302a5f ui: Fix Source & Raw HTML tab nav 2019-02-17 08:55:59 -08:00
James Hillyerd
35969e0b0f ui: Reorg navbar CSS 2019-02-17 08:55:59 -08:00
James Hillyerd
d933d591d8 ui: Add button to open recent menu via keyboard 2019-02-15 21:28:29 -08:00
James Hillyerd
b82cafc338 ui: mailbox input to end of nav for kbd sanity 2019-02-15 18:47:05 -08:00
James Hillyerd
f739ba90a1 Merge branch 'feature/mobile-msg' into develop 2019-02-09 13:56:42 -08:00
James Hillyerd
6724c86181 ui: Add message back/close button for mobile 2019-02-09 13:44:46 -08:00
James Hillyerd
645feeaf85 ui: convert mailbox.css to mobile-first 2019-02-09 13:16:43 -08:00
James Hillyerd
99df27ee34 ui: extract mailbox.css 2019-02-09 13:16:43 -08:00
James Hillyerd
5ae69314dd Merge branch 'feature/mobile-nav' into develop 2019-01-26 14:29:01 -08:00
James Hillyerd
3df655d611 ui: Fix desktop navbar rendering 2019-01-26 14:27:38 -08:00
James Hillyerd
ae76ecef00 ui: Passable mobile menu, but desktop broken 2019-01-26 13:26:16 -08:00
James Hillyerd
37f05b08c5 ui: Extract navbar.css 2019-01-26 11:11:38 -08:00
James Hillyerd
79fdc58567 docker: set UIDIR, closes #134 2019-01-21 10:49:19 -08:00
James Hillyerd
d16699f59f travis: Build Elm UI during deploy 2019-01-06 18:48:18 -08:00
James Hillyerd
9ca179e249 travis: Make Elm part of the build matrix 2019-01-06 18:09:52 -08:00
James Hillyerd
07e75495e8 build: Update goreleaser for Elm UI 2019-01-06 13:57:20 -08:00
James Hillyerd
683ce1241e ui: Comma separate To list 2019-01-06 11:30:45 -08:00
James Hillyerd
9815a66575 ui: Implement clear button on monitor 2019-01-01 14:45:25 -08:00
James Hillyerd
8e04ce1fec ui: Fix monitor scrolling 2019-01-01 14:27:31 -08:00
James Hillyerd
6287f5fe9c ui: Truncate monitor after 500 messages, closes #87 2019-01-01 13:48:41 -08:00
James Hillyerd
f47e2cfcc2 ui: Reimplement message monitor as web component, closes #128 2019-01-01 13:23:31 -08:00
James Hillyerd
dbdc60a0fb message, webui: Fix QP address rendering, closes #117 2019-01-01 10:04:37 -08:00
James Hillyerd
c0a878db47 config: Remove mailbox prompt, no longer used 2019-01-01 09:36:39 -08:00
James Hillyerd
0ea18cbe2b Merge branch 'feature/layout' into develop 2018-12-31 16:48:30 -08:00
James Hillyerd
986377b531 ui: Clarifying renames 2018-12-31 16:47:49 -08:00
James Hillyerd
fac44b7753 ui: rename Page -> Layout 2018-12-31 16:28:22 -08:00
James Hillyerd
c977ded5ba ui: Reimplement session storage in Main 2018-12-31 16:16:20 -08:00
James Hillyerd
c2109a8df0 Merge branch 'feature/config-cookie' into develop 2018-12-31 15:29:15 -08:00
James Hillyerd
321c5615a5 ui: Respect monitor visible config option 2018-12-31 15:10:33 -08:00
James Hillyerd
c57260349b web + ui: Pass init cookie from server to client 2018-12-31 15:10:05 -08:00
James Hillyerd
91f3e08ce5 ui: Make CSS .well reusable for error/warn 2018-12-28 13:18:14 -08:00
James Hillyerd
c762c4d7a1 Merge branch 'feature/mime-errors' into develop 2018-12-28 11:47:26 -08:00
James Hillyerd
b954bea7c6 ui: Render MIME parsing errors 2018-12-28 11:21:51 -08:00
James Hillyerd
362ece171a webui: Add MIME errors to mailbox message 2018-12-28 10:28:36 -08:00
James Hillyerd
1922dc145d ui: Ouput web fonts to dist/static/ 2018-12-27 10:15:09 -08:00
James Hillyerd
4b9e432730 ui: Fix long message & list scrolling 2018-12-27 09:20:10 -08:00
James Hillyerd
78b36b0b14 Merge branch 'feature/session' into develop 2018-12-26 15:44:23 -08:00
James Hillyerd
2f7194835d ui: Remove Session from Main model 2018-12-26 15:22:45 -08:00
James Hillyerd
7c213cd897 ui: Embed Session into page models 2018-12-26 15:22:13 -08:00
James Hillyerd
6189b56b79 ui: Add fontawesome-free, use in status & mailbox 2018-12-23 15:28:23 -08:00
James Hillyerd
1a8b5184cd Merge branch 'feature/travis' into develop 2018-12-23 12:45:55 -08:00
James Hillyerd
55e11929c7 travis: Eliminate travis-deploy.sh 2018-12-23 12:45:40 -08:00
James Hillyerd
4dd3ad33f9 travis: Add Elm stage 2018-12-23 12:27:41 -08:00
James Hillyerd
92c89b98ee travis: Utilize build stages for deploy 2018-12-23 10:25:06 -08:00
James Hillyerd
51d732fa20 ui: Add raw HTML button to message 2018-12-15 21:30:13 -08:00
James Hillyerd
ffaf296faa ui: Make the error flash look nice 2018-12-15 21:17:37 -08:00
James Hillyerd
af3ed04100 ui: Move div.page into Page frame 2018-12-15 20:53:13 -08:00
James Hillyerd
caec5e7c17 ui: Add request context for error flash
- webui: Update mailbox, attachment paths
2018-12-15 20:16:20 -08:00
James Hillyerd
862aff434e Merge branch 'release/2.1.0' into master 2018-12-15 12:02:43 -08:00
James Hillyerd
6ef2beb821 CHANGELOG: release 2.1.0 2018-12-15 12:02:23 -08:00
James Hillyerd
6fd13a5215 Merge branch 'feature/cleanup' into develop #133 2018-12-15 10:01:07 -08:00
James Hillyerd
77ea66e0e6 web: Remove unused helpers 2018-12-15 09:51:37 -08:00
James Hillyerd
89886843bd web: Remove template related code 2018-12-15 09:20:57 -08:00
James Hillyerd
4894244d5c webui: Remove sessions & securecookie 2018-12-15 08:35:00 -08:00
James Hillyerd
d627da2038 webui: Remove unused routes/handlers 2018-12-15 08:35:00 -08:00
James Hillyerd
348eebe418 Add basic NixOS shell.nix file 2018-12-15 08:34:07 -08:00
James Hillyerd
bc427e237f ui: Move REST API calls into new Api module 2018-12-14 21:13:13 -08:00
James Hillyerd
f12a72871f ui: Mailbox purge prompts for confirmation 2018-12-12 22:12:35 -08:00
James Hillyerd
efe554bd77 ui: build tweaks
- ignore ui/dist dir
- dev-start.sh prompts for npm instead of elm-app
2018-12-12 20:21:40 -08:00
James Hillyerd
ecd7c9f6e6 Merge branch 'feature/docker' into develop 2018-12-11 19:48:33 -08:00
James Hillyerd
f0c9a1e7f4 ui: Add chunk hash to static js file name 2018-12-11 19:43:22 -08:00
James Hillyerd
1eba3164b5 ui: Remove built assets from repo 2018-12-11 19:25:23 -08:00
James Hillyerd
aae41ab79a docker: Build Elm UI in container 2018-12-11 19:24:38 -08:00
James Hillyerd
fc5cc4d864 ui: Display server uptime, scan completion time 2018-12-10 21:47:34 -08:00
James Hillyerd
9b3049562d ui: Display server configuration on status page 2018-12-09 16:23:15 -08:00
James Hillyerd
7a16f64ff0 ui: Make message & list scroll within viewport 2018-11-25 21:42:43 -08:00
James Hillyerd
6a95dfe5c6 ui: Invert mailbox media queries for desktop/mobile 2018-11-25 20:34:31 -08:00
James Hillyerd
22884378f3 ui: Stop using element IDs for styling 2018-11-25 19:52:28 -08:00
James Hillyerd
0cf97f5c58 ui: Style error flash, add close link 2018-11-25 09:22:59 -08:00
James Hillyerd
4eb2d5ae97 ui: Update browser URL after deleting a message 2018-11-23 17:52:10 -08:00
James Hillyerd
ce59c87250 ui: Eliminate javascript:void, closes #130 2018-11-23 17:26:09 -08:00
James Hillyerd
6215ce77dd ui: Show dates in browser local timezone 2018-11-23 16:47:22 -08:00
James Hillyerd
ba8e2de475 ui: Cleanup routing 2018-11-23 16:11:33 -08:00
James Hillyerd
0f9585a52b ui: Add session update logic into Session 2018-11-23 13:57:42 -08:00
James Hillyerd
e71377f966 ui: Refactor update and setRoute with updateWith 2018-11-23 13:02:10 -08:00
James Hillyerd
0c9cf81c94 main: bump enmime to v0.4.0 2018-11-21 21:45:27 -08:00
James Hillyerd
ff7fb8a781 docker: Fix build for webpack UI dist dir 2018-11-18 19:53:55 -08:00
James Hillyerd
6619764ea2 Merge elm 0.19 upgrade, closes #125 2018-11-18 19:47:31 -08:00
James Hillyerd
0d9952d35f ui: Commit UI build artifacts 2018-11-18 19:45:21 -08:00
James Hillyerd
5be2b57a12 ui: Easy renames and refactors 2018-11-18 19:41:32 -08:00
James Hillyerd
0ed0cd2d64 ui: Production optimization, basic babel config 2018-11-18 16:09:03 -08:00
James Hillyerd
74e7fd1179 ui: Fix external links 2018-11-18 11:36:49 -08:00
James Hillyerd
eaf41949d4 ui: Refactor page view/framing to handle titles 2018-11-18 10:51:26 -08:00
James Hillyerd
59062e1326 ui: Enable compile-error dev server overlay, SPA routing 2018-11-18 09:18:03 -08:00
James Hillyerd
019bd11309 ui: Parse POSIX millisecond date stamps 2018-11-17 21:11:41 -08:00
James Hillyerd
cf265dbe2c rest: Add posix-millis field for easier date parsing 2018-11-17 20:57:52 -08:00
James Hillyerd
c77cae2429 ui: Update README for npm/webpack 2018-11-17 19:18:39 -08:00
James Hillyerd
abd9ebeb35 ui: Use web components to render server-side HTML 2018-11-17 19:09:19 -08:00
James Hillyerd
f2cd3f92da ui: Upgrade to elm/http 2.0.0 2018-11-17 18:48:52 -08:00
James Hillyerd
e70900dd1a web: Implement SPA compatible routing 2018-11-17 14:38:04 -08:00
James Hillyerd
284dd70bc6 ui: minimal webpack 4 build w/ proxy & hot reload 2018-11-17 14:37:27 -08:00
James Hillyerd
fe20854173 ui: Get UI to compile with Elm 0.19 2018-11-17 14:36:47 -08:00
James Hillyerd
5ccdece541 ui: Remove built assets during elm upgrade 2018-11-17 14:19:10 -08:00
James Hillyerd
b67d5ba376 ui: Reformat with elm-format 0.8.1 2018-11-13 22:06:19 -08:00
James Hillyerd
ecd0c124d4 ui: Re-implement websockets with ports+JS 2018-11-13 21:27:58 -08:00
James Hillyerd
ac3a94412d Merge Elm user interface into develop, closes #105 2018-11-12 22:15:37 -08:00
James Hillyerd
8017e0ce57 docker: Update for elm UI artifacts 2018-11-12 22:05:48 -08:00
James Hillyerd
1f2d1a4622 ui: Commit build artifacts 2018-11-12 21:53:53 -08:00
James Hillyerd
bea3849c97 ui: Update README with dev instructions 2018-11-12 21:26:58 -08:00
James Hillyerd
2bbcef072a ui: Add greeting.html to Home 2018-11-12 20:38:00 -08:00
James Hillyerd
d1954cdd6f ui: Reformat with new elm-format 2018-11-12 20:38:00 -08:00
James Hillyerd
c92cd309bc ui: Add friendly date to Mailbox message view
- Refactor some date stuff
2018-11-12 20:38:00 -08:00
James Hillyerd
d05eb10851 ui: Friendly date format for mailbox list, monitor 2018-11-12 20:38:00 -08:00
James Hillyerd
9e2f138279 ui: Add purge button to Mailbox 2018-11-12 20:38:00 -08:00
James Hillyerd
af9c735cd7 ui: messageList becomes viewMessageList 2018-11-12 20:38:00 -08:00
James Hillyerd
5328406533 ui: Move selected MessageID into MessageList record 2018-11-12 20:38:00 -08:00
James Hillyerd
54ca36c442 ui: Refactor Mailbox states to reduce cases. 2018-11-12 20:38:00 -08:00
James Hillyerd
5ab273b7b8 travis: Remove Go 1.10.x 2018-11-12 20:38:00 -08:00
James Hillyerd
352e8c396d ui: Refactor Mailbox update function 2018-11-12 20:38:00 -08:00
James Hillyerd
f0b4dda8e6 ui: Add search to Mailbox 2018-11-12 20:38:00 -08:00
James Hillyerd
c8dabf8593 ui: Mailbox headers get a MessageList type for search 2018-11-12 20:38:00 -08:00
James Hillyerd
852b9fce26 ui: Refactor mailbox header list into State 2018-11-12 20:38:00 -08:00
James Hillyerd
a8795f46dc ui: Adding Transistioning state to Mailbox to stop flicker 2018-11-12 20:38:00 -08:00
James Hillyerd
bcf0cafb34 ui: Refactor Mailbox model states 2018-11-12 20:38:00 -08:00
James Hillyerd
04a3f58e6d ui: Refactor message/markSeenAt into visible record. 2018-11-12 20:38:00 -08:00
James Hillyerd
7dade7f0e4 ui: Initial impl of seen message marking 2018-11-12 20:38:00 -08:00
James Hillyerd
523c04a522 Logging improvements, handler extraction.
- rest: improve error logging.
- web: extract handlers/middleware into their own file.
- web: log all requests, not just ones hitting our handlers.
- test: improve integration test logging format.
2018-11-12 20:38:00 -08:00
James Hillyerd
7a5459ce08 test: Fix integation test server startup 2018-11-12 20:38:00 -08:00
James Hillyerd
dd14fb9989 ui: Much elm work, such wow
- ui: Fix favicon
- webui: Changes to support serving Elm UI
- Static files now served from `/` mount point.
- Old UI handlers moved to `/serve` mount point, some will still be
  needed by the Elm UI; safe HTML and attachments for example.
- Update dev-start.sh for new UI, with tip on how to build it.
- ui: Detect browser host:port for websocket URL,
- webui: Remove unused mailbox handlers, rename routes
- Many routes not needed by Elm UI.
- `/serve/mailbox/*` becomes `/serve/m/*`.
- webui: Impl custom JSON message API for web UI,
- ui: Refactor Mailbox view functions,
- ui: Add body tabs for safe HTML and plain text,
- webui: Format plain text for new UI,
- ui: List attachments with view & download links,
2018-11-12 20:34:51 -08:00
James Hillyerd
c5b5321be3 ui: Initial Elm UI import
Merged from https://github.com/jhillyerd/inbucket-elm

Uses https://github.com/halfzebra/create-elm-app
2018-11-12 20:34:35 -08:00
James Hillyerd
8b5a05eb40 ui: remove old jquery/HTML template based UI 2018-11-12 20:34:27 -08:00
James Hillyerd
60db73b813 test: Correctly shutdown integration test server 2018-11-03 18:53:24 -07:00
James Hillyerd
ef633b906c travis: remove branch master check for deploy 2018-10-31 20:51:33 -07:00
James Hillyerd
62406f05e8 travis: remove branch master check for deploy 2018-10-31 20:39:35 -07:00
James Hillyerd
2e49b591eb Merge tag 'v2.1.0-beta1' into develop
v2.1.0-beta1
2018-10-31 20:08:15 -07:00
James Hillyerd
7d7e408bfa Merge branch 'release/2.1.0-beta1' 2018-10-31 20:07:34 -07:00
James Hillyerd
91fea4e1fd Update CHANGELOG for beta 2018-10-31 20:06:47 -07:00
James Hillyerd
469132fe2f rest: Add godoc example test for client.
- Update README and CHANGELOG
2018-10-31 19:45:15 -07:00
James Hillyerd
690b19a22c rest: Rewrite client tests using httptest server. 2018-10-31 18:44:32 -07:00
James Hillyerd
30e3892cb0 webui, rest: Render UTF-8 addresses correctly, fixes #117 2018-10-22 18:29:03 -07:00
James Hillyerd
fcb4bc20e0 test: Add basic integration test suite, closes #119 2018-10-22 16:25:27 -07:00
James Hillyerd
8a3d2ff6a2 storage: Add test for id='latest', implment in mem store. 2018-10-22 15:43:17 -07:00
James Hillyerd
2f67a6922a ui: Update default greeting.html, closes #106. 2018-10-22 12:28:12 -07:00
James Hillyerd
82e6a9fe5d rest: Use a subrouter for /api/ paths 2018-10-22 10:48:08 -07:00
James Hillyerd
1a7e47b60a rest: Make tests easier to read, less logic. 2018-10-22 09:37:15 -07:00
James Hillyerd
4d17886ed6 Merge branch 'feature/retention-perf' into develop 2018-10-21 21:25:09 -07:00
James Hillyerd
0640f9fa08 file: Use os.Readdirnames to eliminate Lstat calls for #122
- This a speed/syscall optimization, not memory.
2018-10-21 09:25:32 -07:00
James Hillyerd
f68f07d896 file: pool index readers to reduce allocs for #122 2018-10-20 20:39:14 -07:00
James Hillyerd
98745b3bb9 web: Optionally mount /debug/pprof for #120
- web: eliminate use of http.DefaultServeMux
2018-10-20 16:16:09 -07:00
James Hillyerd
5e8f00fe0b Merge branch 'feature/modules-121' into develop 2018-10-20 12:37:43 -07:00
James Hillyerd
f9adced65e docker: Update build to use Go 1.11+modules for #121
- Fix outdated params in docker-run.sh
2018-10-20 12:36:09 -07:00
James Hillyerd
dc007da82e build: Use go modules for #121
- travis: Bump release trigger env to 1.11
2018-10-20 11:58:45 -07:00
James Hillyerd
bf12925fd1 travis: golint & golang updates 2018-10-20 11:18:48 -07:00
James Hillyerd
0d7c94c531 smtp: add missing log message param 2018-10-20 11:13:39 -07:00
kingforaday
00dad88bde Fixing an erroneous connection close introduced in #98. (#101) 2018-05-20 09:51:40 -07:00
James Hillyerd
fdcb29a52b smtp: rename readByteLine to readDataBlock for #98.
Update change log.
2018-05-06 12:09:55 -07:00
kingforaday
894db04d70 Opportunistic TLS Support (#98)
* STARTTLS Support, disabled by default.
* Added documentation
2018-05-06 11:56:38 -07:00
James Hillyerd
58c3e17be7 Merge tag 'v2.0.0' into develop
v2.0.0 release
2018-05-05 10:17:43 -07:00
James Hillyerd
30d8d6c64f Merge branch 'release/2.0.0' 2018-05-05 10:17:19 -07:00
James Hillyerd
37361e08e8 Change log release prep 2018-05-05 10:13:20 -07:00
James Hillyerd
2ceb510f70 Update change log 2018-05-05 10:08:40 -07:00
James Hillyerd
62fa52f42c log: disable color output on windows, closes #93 2018-05-05 09:57:01 -07:00
James Hillyerd
568474da32 travis: back to 1.10.x 2018-05-05 09:36:46 -07:00
evilmrburns
562332258d Fix INBUCKET_STORAGE_PARAMS example (#95) 2018-04-21 10:02:45 -07:00
evilmrburns
941b682197 Fix INBUCKET_STORAGE_PARAMS example (#95) 2018-04-21 10:01:10 -07:00
James Hillyerd
7fc5e06517 travis: install rpm 2018-04-07 20:35:37 -07:00
James Hillyerd
704ba04c51 travis: specify go 1.10.1 2018-04-07 20:35:19 -07:00
James Hillyerd
a291944a7d travis: install rpm 2018-04-07 20:22:38 -07:00
James Hillyerd
7afc49d88f travis: specify go 1.10.1 2018-04-07 20:07:49 -07:00
James Hillyerd
8a30b9717e Merge tag 'v2.0.0-rc1' into develop
v2.0.0-rc1 see CHANGELOG.md
2018-04-07 19:36:34 -07:00
James Hillyerd
61ac42379e Merge branch 'release/2.0.0-rc1' 2018-04-07 19:36:13 -07:00
James Hillyerd
1ed8723bd7 2.0.0-rc1 changelog update 2018-04-07 19:33:43 -07:00
James Hillyerd
bcede38453 webui: Friendly URL support for #73 2018-04-07 18:17:39 -07:00
James Hillyerd
7e71b4a42c Move swaks-tests into etc for #79 2018-04-07 17:22:44 -07:00
James Hillyerd
e8f57fb4ed Update changelog for mailbox naming 2018-04-07 17:17:37 -07:00
James Hillyerd
d846f04186 docker: Switch to multistage build, closes #66 2018-04-07 17:00:37 -07:00
James Hillyerd
7a783efd5d config: Document MailboxNaming for #33 2018-04-07 16:20:00 -07:00
James Hillyerd
a40c92d221 Merge branch 'feature/fullemail-33' for #33 2018-04-07 14:49:33 -07:00
James Hillyerd
c6bb7d1d4d Fix some null pointers during message retrieval 2018-04-07 14:40:54 -07:00
James Hillyerd
12f98868ba smtp/policy: Store messages with full naming for #33
- Added some tests to make sure stores do not enforce their own naming.
- Improve recipient structured logging.
2018-04-07 13:06:56 -07:00
James Hillyerd
bf152adbef Use enum for MailboxNaming for #33 2018-04-04 21:55:52 -07:00
James Hillyerd
ff2121fbb9 policy: Add support for MailboxNaming to ExtractMailbox for #33 2018-04-04 20:22:40 -07:00
James Hillyerd
939ff19991 policy: Tie mailbox extraction to Addressing for #33 2018-04-02 20:15:50 -07:00
James Hillyerd
c2e1d58b90 policy: Accept Root config instead of SMTP for #33 2018-04-02 19:43:05 -07:00
James Hillyerd
8c66a24513 config: lowercase log levels for #90 2018-04-01 20:50:18 -07:00
James Hillyerd
d1dbcf6e63 Merge branch 'feature/whitelist-51', closes #51 2018-04-01 20:02:56 -07:00
James Hillyerd
064549f576 policy: Implement store domain policy for #51
- Update docs, change log, status.html
2018-04-01 19:59:04 -07:00
James Hillyerd
a7d2b00a9c policy: Implement recipient domain policy for #51
- INBUCKET_SMTP_DEFAULTACCEPT
- INBUCKET_SMTP_ACCEPTDOMAINS
- INBUCKET_SMTP_REJECTDOMAINS
2018-04-01 18:05:21 -07:00
James Hillyerd
0b3c18eba9 Merge branch 'feature/read-58' for #58 2018-04-01 15:32:36 -07:00
James Hillyerd
c695a2690d ui: Mark messages as seen after 1.5s for #58
Embolden subject font for unseen messages.
2018-04-01 15:16:48 -07:00
James Hillyerd
dc02092cf6 rest: Implement MarkSeen for #58
- message: Add MarkSeen to Manager, StoreManager.
- rest: Add PATCH for /mailbox/name/id.
- rest: Add MailboxMarkSeenV1 handler.
- rest: Add Seen to model.
- rest: Update handlers to set Seen.
- rest: Add doJSONBody func.
2018-04-01 13:30:43 -07:00
James Hillyerd
cc5cd7f9c3 storage: Add Seen flag, tests for #58 2018-04-01 13:30:43 -07:00
James Hillyerd
e3be5362dc dev-start.sh: update TRACE to DEBUG 2018-04-01 13:30:01 -07:00
James Hillyerd
3fe4140733 pop3, smtp: embed Server struct into Session for #91 2018-03-31 17:09:30 -07:00
James Hillyerd
7b073562eb pop3: Use config.POP3 directly in server #91 2018-03-31 17:01:02 -07:00
James Hillyerd
2c813081eb smtp: Use config.SMTP directly in Server #91 2018-03-31 16:49:52 -07:00
James Hillyerd
acd48773da Merge branch 'feature/zerolog' into develop, closes #90 2018-03-31 16:17:04 -07:00
James Hillyerd
87bab63aa2 docker: Default to JSON log output for #90 2018-03-31 16:12:05 -07:00
James Hillyerd
47b526824b travis: Move to Go 1.10.x 2018-03-31 15:36:39 -07:00
James Hillyerd
5a28e9f9e7 config: Use log level name DEBUG instead of TRACE
Add log level parsing into openLog() for #90
2018-03-31 15:30:36 -07:00
James Hillyerd
deceb29377 inbucket: respect -logfile flag again for #90
Removed log file rotation, too racy, not needed in the world of docker
and systemd.
2018-03-31 15:16:22 -07:00
James Hillyerd
e076f80416 smtp: Use zerolog hooks for warns/errors expvars #90 2018-03-31 14:06:58 -07:00
James Hillyerd
92f2da5025 server: -netdebug flag now controls tracing for #90
Network trace is sent to stdout, no longer part of normal debug
logging.
2018-03-31 13:37:42 -07:00
James Hillyerd
cbdb96a421 log: package deleted for #90 2018-03-31 12:25:54 -07:00
James Hillyerd
6601d156be metric: new pkg refactored from log for #90 2018-03-31 12:16:54 -07:00
James Hillyerd
779b1e63af smtp, pop3: Use zerolog for session logging #90 2018-03-27 21:52:28 -07:00
James Hillyerd
6f25a1320e pop3, smtp: rename Session method receivers to s 2018-03-27 20:51:13 -07:00
James Hillyerd
e2ba10c8ca Replace pkg/log with zerolog for normal logging #90 2018-03-27 20:26:06 -07:00
James Hillyerd
64ecd810b4 Merge branch 'feature/distro-pkgs' into develop, closes #89 2018-03-25 20:19:59 -07:00
James Hillyerd
393a5b8d4e redhat: Use goreleaser to generate .rpm package for #89 2018-03-25 20:13:19 -07:00
James Hillyerd
0055b84916 debian: Use goreleaser to generate .deb package for #89 2018-03-25 19:34:51 -07:00
James Hillyerd
7ab9ea92ad Merge branch 'feature/envconfig' into develop 2018-03-25 17:05:52 -07:00
James Hillyerd
06989c8218 Update goreleaser config for #86
- Remove inbucket.bat, with new env defaults Windows does not need a
  script to launch.
2018-03-25 16:52:29 -07:00
James Hillyerd
23dc357202 etc: Add dev-start.sh script for #86 2018-03-25 16:16:06 -07:00
James Hillyerd
2d09e94f87 log: Fix another deadlock. 2018-03-25 16:08:34 -07:00
James Hillyerd
86c8ccf9ea docker: Update for environment config for #86
- Change to default ports (less surprising)
- Drop `/con/` volume naming, never caught on
2018-03-25 15:39:04 -07:00
James Hillyerd
ce2339ee9c conf: Delete obsolete config files for #86 2018-03-25 14:30:34 -07:00
James Hillyerd
69a0d355f9 doc: Add doc/config.md to document config for #86
- Increase default max message size to 10MB.
2018-03-25 14:12:18 -07:00
James Hillyerd
04bb842549 config: Combine TemplateDir and PublicDir into UIDir
- Define static names for `templates` and `static`
2018-03-25 11:55:23 -07:00
James Hillyerd
b50c926745 webui: Renamed themes dir to ui
- Eliminated intermediate bootstrap dir
2018-03-25 11:32:59 -07:00
James Hillyerd
0d6936d1b3 Merge branch 'feature/memstore' into develop 2018-03-24 21:07:25 -07:00
James Hillyerd
412b62d6fa storage/mem: implement size enforcer for #88 2018-03-24 20:27:05 -07:00
James Hillyerd
b42ea130ea storage/mem: implement message cap for #88
- Move message cap tests into storage test suite.
- Update change log.
2018-03-24 14:36:23 -07:00
James Hillyerd
281cc21412 storage: Make type/params configurable for #88 2018-03-24 13:18:51 -07:00
James Hillyerd
bb0fb410c1 mem: Initial in-memory store implementation for #88
- Reduce default retention sleep, change description.
2018-03-22 22:29:25 -07:00
James Hillyerd
3c7c24b698 storage: Calculate size of store for status page 2018-03-22 20:30:23 -07:00
James Hillyerd
f0a94f4848 More config cleanups for #86 2018-03-22 20:03:05 -07:00
James Hillyerd
845cbedc0d config: Replace robfig with envconfig for #86
- Initial envconfig system is working, not bulletproof.
- Added sane defaults for required parameters.
2018-03-21 22:50:21 -07:00
James Hillyerd
be940dd2bc rest: fix timezone in controller tests 2018-03-20 19:18:07 -07:00
James Hillyerd
e7a86bd8f8 Hide envelope, use Part.Content for #85 2018-03-20 17:55:43 -07:00
James Hillyerd
6d250a47b4 Merge giant storage/service layer refactor #69 #81 2018-03-18 15:24:21 -07:00
James Hillyerd
e5785e81aa Update CHANGELOG for refactor 2018-03-18 15:14:48 -07:00
James Hillyerd
30f5c163e4 log: Add locking to prevent race 2018-03-18 14:30:56 -07:00
James Hillyerd
0d0e07da70 file: Remove index and dir mutexes
HashLock makes these redundant.  #77
2018-03-18 13:58:47 -07:00
James Hillyerd
5cb07d5780 rest: Refactor JSON result value testing 2018-03-18 12:08:40 -07:00
James Hillyerd
30a329c0d3 Renames, closes #69
- storage: rename StoreMessage to Message
- storage: rename Message.RawReader() to Source()
2018-03-17 17:56:06 -07:00
James Hillyerd
f953bcf4bb smtp: Move delivery into message.Manager for #69 2018-03-17 17:04:12 -07:00
James Hillyerd
a22412f65e manager: Add MailboxForAddress(), calls policy pkg #84 2018-03-17 15:17:44 -07:00
James Hillyerd
dc4db59211 smtp: Don't require MIME headers for metadata
This was a regression, will again fall back to MAIL FROM/RCPT TO data.
2018-03-17 14:41:03 -07:00
James Hillyerd
e84b1f8952 storage: Make locking an implementation detail for #69
- file: Store handles its own locking #77
- file: Move mbox into its own file
- file & test: remove LockFor()
2018-03-17 14:02:50 -07:00
James Hillyerd
b9003a9328 smtp: Wire in policy.Recipient for #84 2018-03-17 12:39:09 -07:00
James Hillyerd
469a778d81 policy: Impl Addressing{} and Recipient{} for #84 2018-03-17 11:16:20 -07:00
James Hillyerd
d132efd6fa policy: Create new policy package for #84 2018-03-17 09:48:53 -07:00
James Hillyerd
9b3d3c2ea8 test: Finish initial storage test suite, closes #82 2018-03-16 22:43:32 -07:00
James Hillyerd
5e13e50763 test: Start work on test suite for #82
- smtp: Tidy up []byte/buffer/string use in delivery #69
2018-03-14 22:51:40 -07:00
James Hillyerd
519779b7ba storage: eliminate mocks, closes #80 2018-03-14 21:05:59 -07:00
James Hillyerd
2cc0da3093 storage: More refactoring for #69
- impl Store.AddMessage
- file: Use AddMessage() in tests
- smtp: Switch to AddMessage
- storage: Remove NewMessage, Append, Close methods
2018-03-14 20:37:20 -07:00
James Hillyerd
9be4eec31c storage: eliminate ReadBody, ReadRaw for #69 2018-03-12 21:23:13 -07:00
James Hillyerd
219862797e web: remove DataStore from Context and controllers for #81 2018-03-12 20:49:06 -07:00
James Hillyerd
10bc07a18e message: Implement service layer, stubs for #81
I've made some effort to wire the manager into the controllers, but
tests are currently failing.
2018-03-12 20:21:59 -07:00
James Hillyerd
3bc66d2788 storage: Store addresses as mail.Address for #69 2018-03-11 16:57:53 -07:00
James Hillyerd
487e491d6f storage: Message refactoring for #69
- Message interface renamed to StoreMessage
- Message.Delete becomes Store.RemoveMessage
- Added deleted message tracking to Store stub for #80
2018-03-11 15:01:40 -07:00
James Hillyerd
12ad0cb3f0 storage: Eliminate storage.Mailbox interface for #69
storage/file Mailbox has been renamed mbox, and is now just an
implementation detail.
2018-03-11 11:54:35 -07:00
James Hillyerd
137466f89b storage: Move NewMessage() into Store interface for #69 2018-03-11 10:48:50 -07:00
James Hillyerd
d9b5e40c87 storage: More refactoring for #69
- retention: Start from pkg main instead of server/smtp
- file: Remove DefaultStore() constructor
- storage: AllMailboxes replaced with VisitMailboxes for #69
- test: Stub VisitMailboxes for #80
2018-03-10 22:05:10 -08:00
James Hillyerd
9c18f1fb30 Large refactor for #69
- makefile: Don't refresh deps automatically, causes double build
- storage: Move GetMessage, GetMessages (Mailbox), PurgeMessages to the
  Store API for #69
- storage: Remove Mailbox.Name method for #69
- test: Create new test package for #79
- test: Implement StoreStub, migrate some tests off MockDataStore for
  task #80
- rest & webui: update controllers to use new Store methods
2018-03-10 19:34:51 -08:00
James Hillyerd
a58dfc5e4f storage: finish renaming storage packages for #79 #69
- storage: rename DataStore to Store
- file: rename types to appease linter
2018-03-10 14:00:21 -08:00
James Hillyerd
98d8288244 Merge branch 'feature/pkg' into develop, closes #79 2018-03-10 13:02:44 -08:00
James Hillyerd
1f56e06fb9 docker: fix build for #79
- Build with Go 1.10
- install.sh: git fetch fails with ssh remotes, removed
2018-03-10 12:45:56 -08:00
James Hillyerd
0016c6d5df readme: updated for reorg, #79
- noted Homebrew is broken
2018-03-10 12:15:05 -08:00
James Hillyerd
94167fa313 Resolve linter errors exposed by fixed Makefile
- TravisCI didn't like "POSIX" ::= syntax
2018-03-10 12:04:13 -08:00
James Hillyerd
f8c30a678a Reorganize packages, closes #79
- All packages go into either cmd or pkg directories
- Most packages renamed
- Server packages moved into pkg/server
- sanitize moved into webui, as that's the only place it's used
- filestore moved into pkg/storage/file
- Makefile updated, and PKG variable use fixed
2018-03-10 12:04:13 -08:00
James Hillyerd
68cfd33fbe Merge tag 'v1.3.1' into develop
See CHANGELOG.md
2018-03-10 10:14:08 -08:00
James Hillyerd
f00b9ddef0 Merge branch 'release/1.3.1' 2018-03-10 10:11:50 -08:00
James Hillyerd
019e66d798 Update change log for 1.3.1 2018-03-10 10:06:09 -08:00
James Hillyerd
a3877e4f4b datastore: Concurrency fix, closes #77 2018-03-09 14:02:15 -08:00
James Hillyerd
a89b6bbca2 Fix change log tag format 2018-02-28 14:22:47 -08:00
James Hillyerd
f58e51d921 Fix change log tag format 2018-02-28 14:21:39 -08:00
James Hillyerd
c39d5ded3f Merge tag 'v1.3.0' into develop
release 1.3.0
2018-02-28 14:19:31 -08:00
James Hillyerd
5f5a7eecd3 Release v1.3.0 2018-02-28 14:18:04 -08:00
James Hillyerd
1ff8ffe9bd Release prep for 1.3.0 2018-02-28 12:50:39 -08:00
James Hillyerd
b4abdb6675 Change to trash glyph for delete mailbox 2018-02-28 12:37:56 -08:00
James Hillyerd
ffa756d895 gcloud: removed
- Dockerized and moved to https://github.com/jhillyerd/demo.inbucket.org
- Merge master changelog entry
2018-02-27 21:20:23 -08:00
James Hillyerd
d5aea4d635 Merge branch 'feature/sanitize'
- Closes #5
- Closes #70
2018-02-27 20:53:38 -08:00
James Hillyerd
3c19e0820b Add Makefile for developer convenience. 2018-02-27 20:50:09 -08:00
James Hillyerd
3b9af85924 sanitize: naive CSS sanitizer implementation
- CSS sanitizer allows a limited set of properties in a style attribute.
- Added a CSS inlined version of the tutsplus responsive test mail.
- Linter fixes in inbucket.go
2018-02-27 20:37:24 -08:00
James Hillyerd
26c38b1148 Simple HTML sanitizer implementation 2018-01-06 16:45:12 -08:00
James Hillyerd
3062b70ea0 Merge branch 'release/1.2.0' 2017-12-27 13:29:06 -08:00
James Hillyerd
01d51302c4 Prepare release 1.2.0 2017-12-27 13:18:11 -08:00
James Hillyerd
dedd0eacff Merge branch 'feature/filestore' into develop #67 2017-12-26 23:17:01 -08:00
James Hillyerd
6431b71cfe Refactor filestore into a dedicated pkg, closes #67 2017-12-26 23:04:39 -08:00
James Hillyerd
25815767a7 Move smtpd/utils.go into dedicated stringutil pkg 2017-12-26 22:55:20 -08:00
James Hillyerd
06165cb3d3 Many linter fixes for smtpd pkg 2017-12-26 22:16:47 -08:00
James Hillyerd
ac21675bd7 Clean up datastore related linter findings 2017-12-26 18:54:02 -08:00
James Hillyerd
f62eaa31b9 Move retention scanner into datastore pkg for #67 2017-12-26 18:33:56 -08:00
James Hillyerd
fcc0848bc0 Move metrics ticker to log pkg for #67 2017-12-26 18:25:11 -08:00
James Hillyerd
dec67622ba Move handler tests to shared datastore mocks for #48 2017-12-26 16:42:25 -08:00
James Hillyerd
11033a5359 Move datastore mocks into correct package
- Start of work for #48
- Continues #67
2017-12-26 15:45:18 -08:00
James Hillyerd
3a4fd3f093 Refactor datastore into it's own package for #67 2017-12-26 14:54:49 -08:00
James Hillyerd
cc47895d71 Pass cfg and ds as params, helps #26 #67 2017-12-26 13:57:04 -08:00
adrium
76a77beca9 Reverse message display sort order (#59)
Closes #60
2017-12-24 13:59:04 -08:00
James Hillyerd
81eba8f51a Only deploy with one version of Go 2017-12-24 13:43:22 -08:00
James Hillyerd
c750dcff81 Merge branch 'hotfix/build' to prevent dup deploys 2017-12-24 13:40:30 -08:00
James Hillyerd
de75b778c0 Only deploy with one version of Go 2017-12-24 13:37:47 -08:00
James Hillyerd
0e72b414c4 Add fauxmailer to gcloud, custom greeting 2017-12-24 13:22:51 -08:00
James Hillyerd
52de1b2bfe Initial gcloud setup.sh, not yet tested as metadata 2017-12-23 23:22:51 -08:00
James Hillyerd
b28e1d86d8 Include version for final goxc release 2017-12-18 19:15:51 -08:00
James Hillyerd
f4fadd7e44 Docker version will now fall back to commit if no tag 2017-12-18 19:12:47 -08:00
James Hillyerd
28b40eb94d Fetch tags during docker build 2017-12-18 19:12:32 -08:00
James Hillyerd
0f67e51e56 Fix version & date in Docker containers for #64 2017-12-18 19:11:08 -08:00
James Hillyerd
9d68e2c0a5 Docker version will now fall back to commit if no tag 2017-12-17 21:44:20 -08:00
James Hillyerd
5bca2ae738 Fetch tags during docker build 2017-12-17 21:29:57 -08:00
James Hillyerd
10cce5c751 Fix version & date in Docker containers for #64 2017-12-17 21:05:48 -08:00
Carlos Tadeu Panato Junior
8040b07c28 Button to delete the mailbox from the UI (#65), closes #55 2017-12-17 20:36:14 -08:00
James Hillyerd
4e8c287608 Migrate from goxc to goreleaser, closes #64 2017-12-17 20:18:51 -08:00
James Hillyerd
6f57c51934 Update release procedures, cleanup goxc config 2017-12-17 20:13:14 -08:00
James Hillyerd
a457b65603 Add cmd/client to release builds 2017-12-17 20:05:07 -08:00
James Hillyerd
890d8e0202 Rename link variables, setup travis tag releases 2017-12-17 19:32:05 -08:00
James Hillyerd
9f6dee640e Customize goreleaser to get a working build 2017-12-17 19:10:59 -08:00
James Hillyerd
095796c8a1 Default config from goreleaser init 2017-12-17 12:33:09 -08:00
James Hillyerd
db358fea8c Merge tag '1.2.0-rc2' into develop 2017-12-15 20:41:02 -08:00
James Hillyerd
86554a63b8 Merge branch 'release/1.2.0-rc2' 2017-12-15 20:39:37 -08:00
James Hillyerd
1efe2ba48f Prepare release 1.2.0-rc2 2017-12-15 20:34:27 -08:00
James Hillyerd
f597687aa3 Update CHANGELOG.md 2017-12-15 20:20:27 -08:00
Carlos Tadeu Panato Junior
6368e3a83b Add option to get the latest message using latest as request parameter (#63) 2017-12-15 17:00:09 -08:00
James Hillyerd
ef17ad9074 Update Docker base to go 1.9 2017-12-14 22:16:24 -08:00
James Hillyerd
7908e41212 Fixes #61 - monitor.history=0 panic 2017-12-14 18:54:22 -08:00
James Hillyerd
a9b174bcb6 Add tl;dr to CONTRIBUTING.md 2017-12-14 18:32:55 -08:00
James Hillyerd
dc0b9b325e Use bash for swaks tests, no pipefail in sh 2017-12-12 19:59:33 -08:00
James Hillyerd
0a967f0f21 Update golang versions 2017-12-12 19:57:20 -08:00
James Hillyerd
304a2260e8 Don't close writers with defer 2017-02-12 16:21:44 -08:00
James Hillyerd
9fc9a333a6 Run travis tests with race detector enabled 2017-02-12 14:53:42 -08:00
James Hillyerd
3e8b914f89 Merge branch 'feature/cmdline' into develop 2017-02-05 15:47:40 -08:00
James Hillyerd
5e94f7b750 Address matching should only apply to address, not name 2017-02-05 15:31:31 -08:00
James Hillyerd
64e75face8 Add maxage flag to match subcommand 2017-02-05 15:15:28 -08:00
James Hillyerd
be4675b374 Add powerful match subcommand to cmdline client
- Multiple output formats
- Signals matches via exit status for shell scripts
- Match against To, From, Subject via regular expressions
- Can optionally delete matched messages
2017-02-05 14:17:47 -08:00
James Hillyerd
6722811425 Beginnings of a command line REST client 2017-02-04 18:21:55 -08:00
James Hillyerd
56cff6296a Update changelog 2017-02-04 18:20:27 -08:00
James Hillyerd
a1e35009e0 Add convenience methods to rest/client types 2017-02-04 16:14:40 -08:00
James Hillyerd
cc0428ab9b Merge branch 'release/1.2.0-rc1' into develop 2017-01-29 13:18:19 -08:00
James Hillyerd
68e35b5eca Merge branch 'release/1.2.0-rc1' 2017-01-29 13:14:55 -08:00
James Hillyerd
5147865e55 rc1 prep 2017-01-29 13:14:27 -08:00
James Hillyerd
a3727ee436 Finalize changelog 2017-01-29 12:41:19 -08:00
James Hillyerd
9e49480482 Update bootstrap theme javascript dependencies 2017-01-28 20:20:58 -08:00
James Hillyerd
958f5a44d9 travis to Go 1.7.5 2017-01-28 19:24:01 -08:00
James Hillyerd
9b1d28fc7d Reimplement msghub as an actor 2017-01-28 19:20:06 -08:00
James Hillyerd
e6f95c9367 Make @inbucket prompt configurable, closes #31 2017-01-28 17:27:50 -08:00
James Hillyerd
de5b9a824b Remove empty intermediate directories, closes #12 2017-01-28 16:17:25 -08:00
James Hillyerd
9ac3c90036 Add mutex to protect directory operations 2017-01-22 22:03:56 -08:00
James Hillyerd
85e3a77fe5 Extract FileMessage into filemsg.go 2017-01-22 21:26:47 -08:00
James Hillyerd
32631daeae Refactor retention scanner prior to starting #12 2017-01-22 20:59:59 -08:00
James Hillyerd
62b77dfe5e Refactor configuration parser to use tables 2017-01-22 18:57:03 -08:00
James Hillyerd
fa28fa57f8 Note Golang 1.7 requirement in CHANGELOG 2017-01-22 14:02:31 -08:00
James Hillyerd
00e4d3791c Allow monitoring of a particular mailbox for #44
- No UI to access, just append /mailbox to /monitor URL
- Changed API URLs:
  - /api/v1/monitor/messages - all
  - /api/v1/monitor/messages/{name} - specific
2017-01-22 13:51:55 -08:00
James Hillyerd
cf7bdee925 Add goroutine count to metrics 2017-01-22 12:45:53 -08:00
James Hillyerd
83b71334c2 Add screenshot to README 2017-01-22 12:02:28 -08:00
James Hillyerd
aa0edff398 Update CHANGELOG with Monitor feature 2017-01-21 21:34:01 -08:00
James Hillyerd
f09a4558a9 Bump go versions for Docker and TravisCI 2017-01-21 21:22:29 -08:00
James Hillyerd
1137912e1d Merge in branch for msg monitor feature, closes #44 2017-01-21 21:19:44 -08:00
James Hillyerd
e14e97919f Add connection status indicator for #44 2017-01-21 20:47:10 -08:00
James Hillyerd
9ae428ca44 Make monitor configurable for #44 2017-01-21 20:23:04 -08:00
James Hillyerd
c346372c85 Fix for flashes not clearing bug 2017-01-21 19:31:39 -08:00
James Hillyerd
63a76696bf Add user interface for monitor, #44 2017-01-16 21:30:40 -08:00
James Hillyerd
86365a047c Add open WebSockets to metrics 2017-01-16 18:53:24 -08:00
James Hillyerd
e5aad9f5d0 Implement server side of message monitor for #44 2017-01-16 18:12:27 -08:00
James Hillyerd
e32e6d00d6 Remove httpbuf, manually save sessions
- httpbuf prevents connections being upgraded to websockets
2017-01-16 16:14:01 -08:00
James Hillyerd
b3db619db9 Broadcast deliveries into msghub for #44 2017-01-16 13:09:50 -08:00
James Hillyerd
6ca2c27747 Pull message delivery into its own method 2017-01-16 10:50:28 -08:00
James Hillyerd
88ccb19360 Implement pub/sub message hub as a base for #44 2017-01-15 22:06:39 -08:00
James Hillyerd
a222b7c428 Make use of pkg context
- Use context inside of servers for shutdown
- Remove unnecessary localShutdown related code
2017-01-15 22:00:58 -08:00
James Hillyerd
0e02061c4a Merge branch 'feature/rest-client' into develop 2017-01-08 22:16:26 +00:00
James Hillyerd
c8fd56ca90 Complete REST client for #43
- Add source, delete and purge
2017-01-08 22:11:22 +00:00
James Hillyerd
d8255382da Start of V1 REST client for #43
- List mailbox contents
- Get message
2017-01-08 04:25:50 +00:00
James Hillyerd
61e9b91637 Generic REST client (HTTP GET only) for #43 2017-01-08 04:11:06 +00:00
James Hillyerd
fa6b0a3227 Move REST JSON model into its own package for #43 2017-01-05 05:59:12 +00:00
James Hillyerd
61c6e7c2e9 Create systemd service unit file for #47
- Switch ubuntu from upstart to systemd
- Switch redhat from init.d to systemd
2017-01-05 05:29:21 +00:00
James Hillyerd
dcc0f36f48 Increase max local-part length to 128
- Improves compatibility with Mailgun
- Closes #41
- Update CHANGELOG
2016-12-31 05:09:12 +00:00
James Hillyerd
c1e7de5e14 Remove old REST API, closes #28 2016-12-31 03:39:32 +00:00
James Hillyerd
493efb04cd Remove legacy theme 'integral' 2016-12-31 01:41:13 +00:00
James Hillyerd
ff481c56c6 Add contributing guide 2016-12-31 00:35:59 +00:00
WiszniewskiMateusz
2f5d80a521 Added data about message attachments to REST API (#46) 2016-12-29 10:13:27 -08:00
James Hillyerd
364e7a0b80 Track enmime API changes
- Part accessors: 196b2ad725
- IsTextFromHTML: 2bd44ac6cc
2016-11-21 22:32:29 -08:00
James Hillyerd
26a9903492 Track enmime API change
0af1249adf
2016-11-21 20:29:45 -08:00
James Hillyerd
264fa9e11d Update travis config for go master 2016-11-17 18:37:37 -08:00
James Hillyerd
1906a147f0 Migrate from pkg go.enmime to enmime 2016-11-17 18:35:01 -08:00
James Hillyerd
145e71dc43 Merge branch 'feature/to-header' into develop
- Messages now store To information
2016-09-22 20:12:09 -07:00
James Hillyerd
017a097588 Switch to storing To addresses as a slice
- Changes on-disk storage format
- Changes JSON API
- To and From values are now parsed/formatted by Go's mail.ParseAddress
  function
- Fixed bug in list-entry-template, was not escaping HTML characters
- Updated tests
2016-09-21 22:12:20 -07:00
James Hillyerd
01ea89e7e2 Multi-recipient swaks test
- Add a multi-recipient test to run-tests.sh
- Removal accidental output of jq binary location when pretty-printing REST JSON
- Add To: change to CHANGELOG.md
- Fix comment typo
2016-09-18 17:48:21 -07:00
Tomasz Wojtuń
8f14ba8359 corrected webui/rest_test.go 2016-09-18 17:48:21 -07:00
Tomasz Wojtuń
8d36aa9750 revert webui stuff 2016-09-18 17:48:21 -07:00
Tomasz Wojtuń
02eee0a608 corrected tests 2016-09-18 17:48:21 -07:00
Tomasz Wojtuń
124f830478 Added "To:" header 2016-09-18 17:48:21 -07:00
James Hillyerd
1856deae46 SMTP handler is now more forgiving of line endings, a la Postfix 2016-09-18 17:45:03 -07:00
James Hillyerd
a939605d4a Cache message in memory during receipt, closes #23 2016-09-18 16:35:13 -07:00
James Hillyerd
f84b36039e Merge branch 'release/1.1.0' into develop 2016-09-03 11:29:39 -07:00
James Hillyerd
5ef3adc88e Merge branch 'release/1.1.0' 2016-09-03 11:27:24 -07:00
James Hillyerd
6d2c2c8dad Prep for 1.1.0 2016-09-03 11:23:09 -07:00
James Hillyerd
ebde99949e Update travis build to current Go versions 2016-09-03 10:25:44 -07:00
James Hillyerd
0afaf5109e Merge pull request #36 from stepstone-tech/issue-35
Log error and continue when deleting oldest message fails - closes #35
2016-04-27 12:30:12 -07:00
Tomasz Wojtuń
7adf3741d3 #35 Log error and continue when deleting oldest message fails. 2016-04-27 17:54:17 +02:00
James Hillyerd
9821095977 Move homebrew to its own tap repository. 2016-03-21 20:08:31 -07:00
James Hillyerd
221a65cbe6 Merge branch 'feature/brew' into develop 2016-03-11 18:35:56 -08:00
James Hillyerd
c421e4e0eb First attempt at a homebrew formula 2016-03-11 18:31:53 -08:00
James Hillyerd
0068937d58 Add homebrew specific inbucket.conf 2016-03-08 23:11:03 -08:00
James Hillyerd
268f950e01 Merge branch 'release/1.1.0-rc2' into develop 2016-03-06 16:48:24 -08:00
James Hillyerd
1742bf9a34 Merge branch 'release/1.1.0-rc2' 2016-03-06 16:47:45 -08:00
James Hillyerd
470ef9b496 Prep for 1.1.0-rc2 2016-03-06 16:47:19 -08:00
James Hillyerd
f16debebbf Handle empty subject lines in bootstrap 2016-03-06 16:31:55 -08:00
James Hillyerd
ff460309e5 Merge branch 'feature/filter' into develop 2016-03-06 15:39:33 -08:00
James Hillyerd
d13ebe9149 Add message list search feature
- Search as you type (client side)
- Add makeDelay to reduce resize and search callback frequency
- Rename class listEntry to message-list-entry
- Move Refresh button into search button bar
2016-03-06 15:35:36 -08:00
James Hillyerd
6fd9f1f98c Load message list over JSON
- Add jquery-load-template to bower
- Add moment (date rendering) to bower
- Load message list JSON via /api/v1/mailbox
- Render message list using jquery template
- Fix resize related problems with message list height caused by
  2092949dbc
2016-03-06 13:15:13 -08:00
James Hillyerd
3481a89533 Display message cap on status page 2016-03-06 10:25:25 -08:00
James Hillyerd
e99baf766b RetentionJoin() could block if scanner never started 2016-03-05 19:19:57 -08:00
James Hillyerd
629bb65cec Host assets for responsive email test 2016-03-05 19:00:07 -08:00
James Hillyerd
42b3ba35cb Merge branch 'release/1.1.0-rc1' into develop 2016-03-03 21:41:27 -08:00
James Hillyerd
c2779ff054 Merge branch 'release/1.1.0-rc1' 2016-03-03 21:41:09 -08:00
580 changed files with 24971 additions and 167431 deletions

View File

@@ -6,3 +6,8 @@ inbucket
inbucket.exe
swaks-tests
target
tags
tags.*
ui/dist
ui/elm-stuff
ui/node_modules

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use nix

5
.gitattributes vendored
View File

@@ -1,6 +1,7 @@
# Auto detect text files and perform LF normalization
* text=auto
*.raw -text
* text=auto
*.golden -text
*.raw -text
# Custom for Visual Studio
*.cs diff=csharp

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

88
.github/workflows/build-and-test.yml vendored Normal file
View File

@@ -0,0 +1,88 @@
name: Build and Test
on:
push:
branches:
- main
pull_request:
jobs:
linux-go-build:
runs-on: ubuntu-latest
name: Linux Go ${{ matrix.go }} build
strategy:
matrix:
go:
- '1.25'
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
check-latest: true
- name: Build and test
run: |
go build ./...
go test -race -coverprofile=profile.cov ./...
- name: Send coverage
uses: shogo82148/actions-goveralls@v1
with:
path-to-profile: profile.cov
flag-name: Linux-Go-${{ matrix.go }}
parallel: true
windows-go-build:
runs-on: windows-latest
name: Windows Go build
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: '1.25'
- name: Build
run: go build ./...
- name: Test
run: go test -race -coverprofile="profile.cov" ./...
- name: Send coverage
uses: shogo82148/actions-goveralls@v1
with:
path-to-profile: profile.cov
flag-name: Windows-Go
parallel: true
ui-build:
runs-on: ubuntu-latest
name: UI Build
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20.x'
cache: 'yarn'
cache-dependency-path: ui/yarn.lock
- name: Build frontend
run: |
yarn install --frozen-lockfile --non-interactive
yarn run build
working-directory: ./ui
coverage:
needs:
- linux-go-build
- windows-go-build
name: Test Coverage
runs-on: ubuntu-latest
steps:
- uses: shogo82148/actions-goveralls@v1
with:
parallel-finished: true

71
.github/workflows/docker-build.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: Docker Image
on:
push:
branches:
- main
tags:
- 'v*'
pull_request_review:
types:
- submitted
workflow_dispatch: # allow for manual run
env:
REGISTRY_PUSH: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
jobs:
build:
name: 'Build Container'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
inbucket/inbucket
ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=sha
type=edge,branch=main
flavor: |
latest=auto
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: ${{ env.REGISTRY_PUSH == 'true' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: ${{ env.REGISTRY_PUSH == 'true' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64, linux/arm/v7
push: ${{ env.REGISTRY_PUSH == 'true' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

17
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Lint Go Code
on:
push:
jobs:
golangci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: latest

55
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Build and Release
on:
push:
branches:
- main
tags:
- 'v*'
workflow_dispatch:
jobs:
release:
name: 'Go Releaser'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: '1.25'
check-latest: true
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20.x'
cache: 'yarn'
cache-dependency-path: ui/yarn.lock
- name: Build frontend
run: |
yarn install --frozen-lockfile --non-interactive
yarn run build
working-directory: ./ui
- name: Test build release
uses: goreleaser/goreleaser-action@v6
if: "!startsWith(github.ref, 'refs/tags/v')"
with:
distribution: goreleaser
version: "~> v2"
args: release --snapshot
- name: Build and publish release
uses: goreleaser/goreleaser-action@v6
if: "startsWith(github.ref, 'refs/tags/v')"
with:
distribution: goreleaser
version: "~> v2"
args: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

43
.gitignore vendored
View File

@@ -3,6 +3,9 @@
*.a
*.so
# Emacs messiness.
*~
# Folders
_obj
_test
@@ -21,14 +24,44 @@ _testmain.go
*.exe
# vim swp files
# vim files
*.swp
*.swo
tags
tags.*
# our binary
# Desktop Services Store on macOS
.DS_Store
/.direnv
# Inbucket binaries
/client
/client.exe
/inbucket
/inbucket.exe
/target/**
/dist/**
/cmd/client/client
/cmd/client/client.exe
/cmd/inbucket/inbucket
/cmd/inbucket/inbucket.exe
# local goxc config
.goxc.local.json
# Elm UI
# elm-package generated files
/ui/index.html
/ui/elm-stuff
/ui/tests/elm-stuff
# elm-repl generated files
repl-temp-*
# Distribution
/ui/dist/
# Dependency directories
/ui/node_modules
/ui/.parcel-cache
# Test lua files
/inbucket.lua
# IntelliJ
.idea
inbucket.iml

82
.golangci.yml Normal file
View File

@@ -0,0 +1,82 @@
version: "2"
linters:
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- containedctx
- contextcheck
- copyloopvar
- decorder
- durationcheck
- errchkjson
- errname
- ginkgolinter
- gocheckcompilerdirectives
- gochecksumtype
- gocritic
- goheader
- gomoddirectives
- gomodguard
- goprintffuncname
- gosmopolitan
- grouper
- importas
- inamedparam
- interfacebloat
- loggercheck
- makezero
- mirror
- misspell
- musttag
- nilerr
- noctx
- nolintlint
- nosprintfhostport
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- rowserrcheck
- sloglint
- staticcheck
- tagliatelle
- testableexamples
- testifylint
- thelper
- tparallel
- unparam
- usestdlibvars
- usetesting
- wastedassign
- whitespace
- zerologlint
settings:
tagliatelle:
case:
rules:
json: kebab
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

87
.goreleaser.yml Normal file
View File

@@ -0,0 +1,87 @@
version: 2 # goreleaser version
project_name: inbucket
release:
github:
owner: inbucket
name: inbucket
name_template: '{{.Tag}}'
before:
hooks:
- go mod download
builds:
- id: inbucket
binary: inbucket
env:
- CGO_ENABLED=0
goos:
- darwin
- freebsd
- linux
- windows
goarch:
- amd64
- arm
- arm64
goarm:
- "7"
main: ./cmd/inbucket
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
- id: inbucket-client
binary: inbucket-client
env:
- CGO_ENABLED=0
goos:
- darwin
- freebsd
- linux
- windows
goarch:
- amd64
- arm
- arm64
goarm:
- "7"
main: ./cmd/client
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
archives:
- id: tarball
formats: tar.gz
wrap_in_directory: true
format_overrides:
- goos: windows
formats: zip
files:
- LICENSE*
- README*
- CHANGELOG*
- etc/**
- ui/dist/**
- ui/greeting.html
nfpms:
- formats:
- deb
- rpm
vendor: inbucket.org
homepage: https://www.inbucket.org/
maintainer: github@hillyerd.com
description: All-in-one disposable webmail service.
license: MIT
contents:
- src: "ui/dist/**"
dst: "/usr/share/inbucket/ui"
- src: "etc/linux/inbucket.service"
dst: "/lib/systemd/system/inbucket.service"
type: config|noreplace
- src: "ui/greeting.html"
dst: "/etc/inbucket/greeting.html"
type: config|noreplace
checksum:
name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt'
dist: dist

View File

@@ -1,18 +0,0 @@
{
"ArtifactsDest": "target",
"TasksExclude": [
"pkg-build"
],
"Arch": "amd64",
"Os": "darwin freebsd linux windows",
"ResourcesInclude": "README*,LICENSE*,CHANGELOG*,inbucket.bat,etc,themes",
"PackageVersion": "1.1.0",
"PrereleaseInfo": "rc1",
"ConfigVersion": "0.9",
"BuildSettings": {
"LdFlagsXVars": {
"TimeNow": "main.BUILDDATE",
"Version": "main.VERSION"
}
}
}

9
.luarc.json Normal file
View File

@@ -0,0 +1,9 @@
{
"runtime.version": "Lua 5.1",
"diagnostics": {
"globals": [
"inbucket",
"smtp"
]
}
}

View File

@@ -1,9 +0,0 @@
language: go
sudo: false
before_script:
- go vet ./...
go:
- 1.5.3
- 1.6

View File

@@ -1,8 +1,362 @@
# Change Log
Change Log
==========
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [1.1.0-rc1]
## [Unreleased]
## [v3.1.0] - 2025-07-27
### Added
- Note in logs that a missing Lua script is not an error (#575)
### Fixed
- Accept and handle emails sent with an empty 821.From (#561)
## [v3.1.0-beta3] - 2024-11-02
### Added
- Lua scripting additions:
- Add `SMTPSession` and `BeforeRcptToAccepted` event (#541)
- Add `SMTPResponse` type for extensions (#539)
- Add `RemoteAddr` to `SMTPSession` (#548)
- Context support for REST client (#496)
### Fixed
- Rename Lua `BeforeMailAccepted`, change args (#547)
- pop3: Prevent STLS cmd triggered crashes (#516)
- ui: date-format version, fixes yarn build (#508)
- rework client example to omit `log.Fatal`, breaks defer (#489)
- Rest Client: Allow relative URLs (#477)
## [v3.1.0-beta2] - 2024-02-05
### Added
- Reject mail by origin domain: `INBUCKET_SMTP_REJECTORIGINDOMAINS` (#375)
- Wildcard support (#412)
- Version flag for `inbucket` cmd (#385)
- STLS support for POP3 (#384)
- ForceTLS flag for SMTP (#402)
- Lua scripting additions:
- `logger` API for Lua (#407)
- `before.message_stored` handler (#417, #418)
- `$` is replaced with `:` in filestore paths, for `D:\...` syntax (#449)
- REST Client `transport` support (#463)
### Fixed
- UI & Storage paths in systemd service file (#393)
- Web UI will redirect from `prefix` to `prefix/` (#397)
- Include inlines when listing attachments (#398)
- Fail Inbucket startup if unable to create storage dir (#448)
- Close directory file handles immediately, fixes Windows locking (#457)
## [v3.1.0-beta1] - 2023-02-28
### Added
- Monitor tab updates when messages are deleted (#337)
- Initial framework for extensions
- Initial Lua scripting implementation, supporting events:
- `after.message_deleted`
- `after.message_stored`
- `before.mail_accepted`
- Provide `http` and `json` modules for Lua scripts
### Fixed
- Support for IP address as domain in RCPT TO (#285)
## [v3.0.4] - 2022-10-02
### Fixed
- More flexible support of `AUTH=<>` FROM parameter (#291)
## [v3.0.3] - 2022-08-07
### Fixed
- Support for `AUTH=<>` FROM parameter (#284)
## [v3.0.2] - 2022-07-04
Note: We had to abandon the 3.0.1 release, see the blog post [What happened to
3.0?](https://www.inbucket.org/news/2022/05/whathappenedtothree.html) for
details.
### Changed
- arm Docker builds now rely on amd64 frontend build stage
- Frontend build migrated from npm+webpack to yarn+parcel, node 16
## [v3.0.1-rc2] - 2022-01-23
### Added
- Builds for arm7 and arm64 platforms
### Changed
- Abandoned git-flow process, the `master` branch renamed to `main`
## [v3.0.1-rc1] - 2022-01-17
### Fixed
- GitHub built packages (rpm, deb, tarball) no longer missing UI files (#250)
### Changed
- Update Go dependencies
- Update NPM dependencies
## [v3.0.0] - 2021-09-19
Unchanged from rc4.
## [v3.0.0-rc4] - 2021-08-22
### Fixed
- Various MIME header decoding improvements
### Changed
- Bump Go version to 1.17 (#233)
## v3.0.0-rc3 - 2021-08-01
Unchanaged from 3.0.0-rc2. This release is to update our build automation and
tags for Docker Hub and ghcr.io.
## [v3.0.0-rc2] - 2021-07-31
### Added
- Support for SMTP AUTH (#197, thanks makarchuk)
- Dark mode support (#218, thanks nerones)
### Fixed
- Prevent potential click jacking (#190, thanks stuartskelton)
- Error on 8 character long SMTP commands (#221)
- Allow empty username and password during AUTH (#225)
## [v3.0.0-rc1] - 2020-09-24
### Added
- Refresh button to reload mailbox contents
- Improved keyboard (tab) focus highlights
### Changed
- The UI now includes the Open Sans webfont instead of relying on browser/OS
fonts
## [v3.0.0-beta3] - 2020-09-04
### Added
- Docker `HEALTHCHECK`
- Mouse-out delay to improve pop-up menu navigation
- Support for configurable URL base path with `INBUCKET_WEB_BASEPATH`
### Changed
- Updated frontend and backend dependencies, Docker image base
### Fixed
- Improved layout on mobile and wide displays
- Prevent unexpected input for modal dialogs
- Allow empty SMTP `MAIL FROM:<>`
## [v3.0.0-beta2] - 2019-08-17
### Added
- Ability to name mailboxes after domain of email recipient, set via
`INBUCKET_MAILBOXNAMING`, thanks MatthewJohn.
### Changed
- Updated JavaScript dependencies.
- Updated Go dependencies.
- Updated Docker build: Go to 1.12, and Alpine Linux to 3.10
### Fixed
- URLs to view/download attachments from REST API, #138
- Support for late EHLO, #141
## [v3.0.0-beta1] - 2019-03-14
### Added
- `posix-millis` field to REST message and header responses for easier date
parsing.
### Changed
- Rewrote the user interface from scratch, it's now an Elm powered single page
application.
- Moved the Inbucket repository to its own GitHub organization.
- Update to enmime v0.5.0
## v2.1.0 - 2018-12-15
No change from beta1.
## [v2.1.0-beta1] - 2018-10-31
### Added
- Use Go 1.11 modules for reproducible builds.
- SMTP TLS support (thanks kingforaday.)
- `INBUCKET_WEB_PPROF` configuration option for performance profiling.
- Godoc example for the REST API client.
### Changed
- Docker build now uses Go 1.11 and Alpine 3.8
### Fixed
- Render UTF-8 addresses correctly in both REST API and Web UI.
- Memory storage now correctly returns the newest message when asked for ID
`latest`.
## [v2.0.0] - 2018-05-05
### Changed
- Corrected docs for INBUCKET_STORAGE_PARAMS (thanks evilmrburns.)
- Disabled color log output on Windows, doesn't work there.
## [v2.0.0-rc1] - 2018-04-07
### Added
- Inbucket is now configured using environment variables instead of a config
file.
- In-memory storage option, best for small installations and desktops. Will be
used by default.
- Storage type is now displayed on Status page.
- Store size is now calculated during retention scan and displayed on the Status
page.
- Debian `.deb` package generation to release process.
- RedHat `.rpm` package generation to release process.
- Message seen flag in REST and Web UI so you can see which messages have
already been read.
- Recipient domain accept policy; Inbucket can now reject mail to specific
domains.
- Configurable support for identifying a mailbox by full email address instead
of just the local part (username).
- Friendly URL support: `<inbucket-url>/<mailbox>` will redirect your browser to
that mailbox.
### Changed
- Massive refactor of back-end code. Inbucket should now be both easier and
more enjoyable to work on.
- Changes to file storage format, will require pre-2.0 mail store directories to
be deleted.
- Renamed `themes` directory to `ui` and eliminated the intermediate `bootstrap`
directory.
- Docker build:
- Uses the same default ports as other builds; smtp:2500 http:9000 pop3:1100
- Uses volume `/config` for `greeting.html`
- Uses volume `/storage` for mail storage
- Log output is now structured, and will be output as JSON with the `-logjson`
flag; which is enabled by default for the Docker container.
- SMTP and POP3 network tracing is no longer logged regardless of level, but can
be sent to stdout via `-netdebug` flag.
- Replaced store/nostore config variables with a storage policy that mirrors the
domain accept policy.
### Removed
- No longer support SIGHUP or log file rotation.
## [v1.3.1] - 2018-03-10
### Fixed
- Adding additional locking during message delivery to prevent race condition
that could lose messages.
## [v1.3.0] - 2018-02-28
### Added
- Button to purge mailbox contents from the UI.
- Simple HTML/CSS sanitization; `Safe HTML` and `Plain Text` UI tabs.
### Changed
- Reverse message display sort order in the UI; now newest first.
## [v1.2.0] - 2017-12-27
### Changed
- No significant code changes from rc2
### Added
- `rest/client` types `MessageHeader` and `Message` with convenience methods;
provides a more natural API
- Powerful command line REST
[client](https://github.com/inbucket/inbucket/wiki/cmd-client)
- Allow use of `latest` as a message ID in REST calls
### Changed
- `rest/client.NewV1` renamed to `New`
- `rest/client` package now embeds the shared `rest/model` structs into its own
types
- Fixed panic when `monitor.history` set to 0
## [v1.2.0-rc1] - 2017-01-29
### Added
- Storage of `To:` header in messages (likely breaks existing datastores)
- Attachment list to [GET message
JSON](https://github.com/inbucket/inbucket/wiki/REST-GET-message)
- [Go client for REST
API](https://godoc.org/github.com/inbucket/inbucket/rest/client)
- Monitor feature: lists messages as they arrive, regardless of their
destination mailbox
- Make `@inbucket` mailbox prompt configurable
- Warnings and errors from MIME parser are displayed with message
### Fixed
- No longer run out of file handles when dealing with a large number of
recipients for a single message.
- Empty intermediate directories are now removed when a mailbox is deleted,
leaving less junk on your filesystem.
### Changed
- Build now requires Go 1.7 or later
- Removed legacy `integral` theme, as most new features only in `bootstrap`
- Removed old RESTful APIs, must use `/api/v1` base URI now
- Allow increased local-part length of 128 chars for Mailgun
- RedHat and Ubuntu now use systemd instead of legacy init systems
## [v1.1.0] - 2016-09-03
### Added
- Homebrew inbucket.conf and formula (see README)
### Fixed
- Log and continue when unable to delete oldest message during cap enforcement
## [v1.1.0-rc2] - 2016-03-06
### Added
- Message Cap to status page
- Search-while-you-type message list filter
### Fixed
- Shutdown hang in retention scanner
- Display empty subject as `(No Subject)`
## [v1.1.0-rc1] - 2016-03-04
### Added
- Inbucket now builds with Go 1.5 or 1.6
- Project can build & run inside a Docker container
@@ -15,15 +369,61 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- RESTful API moved to `/api/v1` base URI
- More graceful shutdown on Ctrl-C or when errors encountered
## [1.0] - 2014-04-14
## [v1.0] - 2014-04-14
### Added
- Add new configuration option `mailbox.message.cap` to prevent individual
mailboxes from growing too large.
- Add Link button to messages, allows for directing another person to a
specific message.
[Unreleased]: https://github.com/jhillyerd/inbucket/compare/1.1.0-rc1...develop
[1.1.0-rc1]: https://github.com/jhillyerd/inbucket/compare/1.0...1.1.0-rc1
[1.0]: https://github.com/jhillyerd/inbucket/compare/1.0-rc1...1.0
See http://keepachangelog.com/ for instructions on how to update this file.
[Unreleased]: https://github.com/inbucket/inbucket/compare/v3.1.0...main
[v3.1.0]: https://github.com/inbucket/inbucket/compare/v3.1.0-beta3...v3.1.0
[v3.1.0-beta3]: https://github.com/inbucket/inbucket/compare/v3.1.0-beta2...v3.1.0-beta3
[v3.1.0-beta2]: https://github.com/inbucket/inbucket/compare/v3.1.0-beta1...v3.1.0-beta2
[v3.1.0-beta1]: https://github.com/inbucket/inbucket/compare/v3.0.4...v3.1.0-beta1
[v3.0.4]: https://github.com/inbucket/inbucket/compare/v3.0.3...v3.0.4
[v3.0.3]: https://github.com/inbucket/inbucket/compare/v3.0.2...v3.0.3
[v3.0.2]: https://github.com/inbucket/inbucket/compare/v3.0.1-rc2...v3.0.2
[v3.0.1-rc2]: https://github.com/inbucket/inbucket/compare/v3.0.1-rc1...v3.0.1-rc2
[v3.0.1-rc1]: https://github.com/inbucket/inbucket/compare/v3.0.0...v3.0.1-rc1
[v3.0.0]: https://github.com/inbucket/inbucket/compare/v3.0.0-rc4...v3.0.0
[v3.0.0-rc4]: https://github.com/inbucket/inbucket/compare/v3.0.0-rc2...v3.0.0-rc4
[v3.0.0-rc2]: https://github.com/inbucket/inbucket/compare/v3.0.0-rc1...v3.0.0-rc2
[v3.0.0-rc1]: https://github.com/inbucket/inbucket/compare/v3.0.0-beta3...v3.0.0-rc1
[v3.0.0-beta3]: https://github.com/inbucket/inbucket/compare/v3.0.0-beta2...v3.0.0-beta3
[v3.0.0-beta2]: https://github.com/inbucket/inbucket/compare/v3.0.0-beta1...v3.0.0-beta2
[v3.0.0-beta1]: https://github.com/inbucket/inbucket/compare/v2.1.0...v3.0.0-beta1
[v2.1.0-beta1]: https://github.com/inbucket/inbucket/compare/v2.0.0...v2.1.0-beta1
[v2.0.0]: https://github.com/inbucket/inbucket/compare/v2.0.0-rc1...v2.0.0
[v2.0.0-rc1]: https://github.com/inbucket/inbucket/compare/v1.3.1...v2.0.0-rc1
[v1.3.1]: https://github.com/inbucket/inbucket/compare/v1.3.0...v1.3.1
[v1.3.0]: https://github.com/inbucket/inbucket/compare/v1.2.0...v1.3.0
[v1.2.0]: https://github.com/inbucket/inbucket/compare/1.2.0-rc2...1.2.0
[v1.2.0-rc2]: https://github.com/inbucket/inbucket/compare/1.2.0-rc1...1.2.0-rc2
[v1.2.0-rc1]: https://github.com/inbucket/inbucket/compare/1.1.0...1.2.0-rc1
[v1.1.0]: https://github.com/inbucket/inbucket/compare/1.1.0-rc2...1.1.0
[v1.1.0-rc2]: https://github.com/inbucket/inbucket/compare/1.1.0-rc1...1.1.0-rc2
[v1.1.0-rc1]: https://github.com/inbucket/inbucket/compare/1.0...1.1.0-rc1
[v1.0]: https://github.com/inbucket/inbucket/compare/1.0-rc1...1.0
## Release Checklist
1. Create a release branch
2. Update CHANGELOG.md:
- Ensure *Unreleased* section is up to date
- Rename *Unreleased* section to release name and date
- Add new GitHub `/compare` link
- Update previous tag version for *Unreleased*
3. Run tests
4. Update goreleaser, and then test cross-compile: `goreleaser release --snapshot --clean`
5. Commit changes and merge release PR into main
6. Create new release via GitHub, use CHANGELOG release notes, tag `vX.Y.Z`
7. Push tags and wait for
[GitHub actions](https://github.com/inbucket/inbucket/actions) to complete
-- it will add compiled release assets
See http://keepachangelog.com/ for additional instructions on how to update this file.

31
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,31 @@
# How to Contribute
Inbucket encourages third-party patches. It's valuable to know how other
developers are using the product.
## Getting Started
If you anticipate your issue requiring a large patch, please first submit a
GitHub issue describing the problem or feature. You are also encouraged to
outline the process you would like to use to resolve the issue. I will attempt
to provide validation and/or guidance on your suggested approach.
## Making Changes
Inbucket follows the regular GitHub pattern. Create a topic branch from where
you want to base your work:
Once you are on your topic branch:
1. Make commits of logical units.
2. Add unit tests to exercise your changes.
3. Run `make` to test, vet and confirm your code is formatted correctly.
If you do not have Make installed, please perform these steps manually,
otherwise your PR will not pass our checks.
## Thanks
Thank you for contributing to Inbucket!

View File

@@ -1,25 +1,58 @@
# Docker build file for Inbucket, see https://www.docker.io/
# Inbucket website: http://www.inbucket.org/
# Docker build file for Inbucket: https://www.inbucket.org/
FROM golang:1.6-alpine
MAINTAINER James Hillyerd, @jameshillyerd
### Build frontend
# Due to no official elm compiler for arm; build frontend with amd64.
FROM --platform=linux/amd64 node:20 AS frontend
RUN npm install -g node-gyp
WORKDIR /build
COPY . .
WORKDIR /build/ui
RUN rm -rf .parcel-cache dist elm-stuff node_modules
RUN yarn install --frozen-lockfile --non-interactive
RUN yarn run build
# Configuration (WORKDIR doesn't support env vars)
ENV INBUCKET_SRC $GOPATH/src/github.com/jhillyerd/inbucket
ENV INBUCKET_HOME /opt/inbucket
WORKDIR $INBUCKET_HOME
ENTRYPOINT ["/con/context/start-inbucket.sh"]
CMD ["/con/configuration/inbucket.conf"]
### Build backend
FROM golang:1.25-alpine3.22 AS backend
RUN apk add --no-cache --virtual .build-deps g++ git make
WORKDIR /build
COPY . .
ENV CGO_ENABLED=0
RUN make clean deps
RUN go build -o inbucket \
-ldflags "-X 'main.version=$(git describe --tags --always)' -X 'main.date=$(date -Iseconds)'" \
-v ./cmd/inbucket
### Run in minimal image
FROM alpine:3.22
RUN apk --no-cache add tzdata
WORKDIR /opt/inbucket
RUN mkdir bin defaults ui
COPY --from=backend /build/inbucket bin
COPY --from=frontend /build/ui/dist ui
COPY etc/docker/defaults/greeting.html defaults
COPY etc/docker/defaults/start-inbucket.sh /
# Configuration
ENV INBUCKET_SMTP_DISCARDDOMAINS=bitbucket.local
ENV INBUCKET_SMTP_TIMEOUT=30s
ENV INBUCKET_POP3_TIMEOUT=30s
ENV INBUCKET_WEB_GREETINGFILE=/config/greeting.html
ENV INBUCKET_WEB_COOKIEAUTHKEY=secret-inbucket-session-cookie-key
ENV INBUCKET_WEB_UIDIR=ui
ENV INBUCKET_STORAGE_TYPE=file
ENV INBUCKET_STORAGE_PARAMS=path:/storage
ENV INBUCKET_STORAGE_RETENTIONPERIOD=72h
ENV INBUCKET_STORAGE_MAILBOXMSGCAP=300
# 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'
# Ports: SMTP, HTTP, POP3
EXPOSE 10025 10080 10110
EXPOSE 2500 9000 1100
# Persistent Volumes, following convention at:
# https://github.com/docker/docker/issues/9277
# NOTE /con/context is also used, not exposed by default
VOLUME /con/configuration
VOLUME /con/data
# Persistent Volumes
VOLUME /config
VOLUME /storage
# Build Inbucket
COPY . $INBUCKET_SRC/
RUN "$INBUCKET_SRC/etc/docker/install.sh"
ENTRYPOINT ["/start-inbucket.sh"]
CMD ["-logjson"]

43
Makefile Normal file
View File

@@ -0,0 +1,43 @@
SHELL = /bin/sh
SRC := $(shell find . -type f -name '*.go' -not -path "./vendor/*")
PKGS := $(shell go list ./... | grep -v /vendor/)
.PHONY: all build clean fmt lint reflex simplify test
commands = client inbucket
all: clean test lint build
$(commands): %: cmd/% $(SRC)
go build ./$<
clean:
go clean $(PKGS)
rm -f $(commands)
rm -rf dist
deps:
go get -t ./...
build: $(commands)
test:
go test -race ./...
fmt:
@gofmt -l -w $(SRC)
simplify:
@gofmt -s -l -w $(SRC)
lint:
@echo "gofmt check..."
@test -z "$(shell gofmt -l . | tee /dev/stderr)" || echo "[WARN] Fix formatting issues with 'make fmt'"
@echo "golint check..."
@golint -set_exit_status $(PKGS)
@echo "go vet check..."
@go vet $(PKGS)
reflex:
reflex -r '\.go$$' -- sh -c 'echo; date; echo; go test ./... && echo ALL PASS'

101
README.md
View File

@@ -1,51 +1,96 @@
Inbucket [![Build Status](https://travis-ci.org/jhillyerd/inbucket.png?branch=master)][Build Status]
========
![Build Status](https://github.com/inbucket/inbucket/actions/workflows/build-and-test.yml/badge.svg)
![Docker Image](https://github.com/inbucket/inbucket/actions/workflows/docker-build.yml/badge.svg)
# Inbucket
Inbucket is an email testing service; it will accept messages for any email
address and make them available via web, REST and POP3. Once compiled,
Inbucket does not have an external dependencies (HTTP, SMTP, POP3 and storage
are all built in).
address and make them available via web, REST and POP3 interfaces. Once
compiled, Inbucket does not have any external dependencies - HTTP, SMTP, POP3
and storage are all built in.
A Go client for the REST API is available in
`github.com/inbucket/inbucket/pkg/rest/client` - [Go API docs]
Read more at the [Inbucket Website]
Development Status
------------------
![Screenshot](http://www.inbucket.org/images/inbucket-ss1.png "Viewing a message")
## Development Status
Inbucket is currently production quality: it is being used for real work.
Please see the [Change Log] and [Issues List] for more details.
Please see the [Change Log] and [Issues List] for more details. If you'd like
to contribute code to the project check out [CONTRIBUTING.md].
Building from Source
--------------------
You will need a functioning [Go installation][Google Go] for this to work.
## Docker
Grab the Inbucket source code and compile the daemon:
Inbucket has automated [Docker Image] builds via Docker Hub. The `latest` tag
tracks our tagged releases, and `edge` tracks our potentially unstable
`main` branch.
go get -v github.com/jhillyerd/inbucket
Start the docker image by running:
Edit etc/inbucket.conf and tailor to your environment. It should work on most
Unix and OS X machines as is. Launch the daemon:
```
docker run -d --name inbucket -p 9000:9000 -p 2500:2500 -p 1100:1100 inbucket/inbucket
```
$GOPATH/bin/inbucket $GOPATH/src/github.com/jhillyerd/inbucket/etc/inbucket.conf
Then point your browser to [localhost:9000](http://localhost:9000/)
## Building from Source
You will need functioning [Go] and [Node.js] installations for this to work.
```sh
git clone https://github.com/inbucket/inbucket.git
cd inbucket/ui
yarn install
yarn build
cd ..
go build ./cmd/inbucket
```
For more information on building and development flows, check out the
[Development Quickstart] page of our wiki.
### Configure and Launch
Inbucket reads its configuration from environment variables, but comes with
reasonable defaults built-in. It should work on most Unix and OS X machines as
is. Launch the daemon:
```sh
./inbucket
```
By default the SMTP server will be listening on localhost port 2500 and
the web interface will be available at [localhost:9000](http://localhost:9000/).
The Inbucket website has a more complete guide to
[installing from source][From Source]
See doc/[config.md] for more information on configuring Inbucket, but you will
likely find the [Configurator] tool the easiest way to generate a configuration.
About
-----
Inbucket is written in [Google Go]
## About
Inbucket is written in [Go] and [Elm].
Inbucket is open source software released under the MIT License. The latest
version can be found at https://github.com/jhillyerd/inbucket
version can be found at https://github.com/inbucket/inbucket
[Build Status]: https://travis-ci.org/jhillyerd/inbucket
[Change Log]: https://github.com/jhillyerd/inbucket/blob/master/CHANGELOG.md
[From Source]: http://www.inbucket.org/installation/from-source.html
[Google Go]: http://golang.org/
[Inbucket Website]: http://www.inbucket.org/
[Issues List]: https://github.com/jhillyerd/inbucket/issues?state=open
[Build Status]: https://travis-ci.org/inbucket/inbucket
[Change Log]: https://github.com/inbucket/inbucket/blob/main/CHANGELOG.md
[config.md]: https://github.com/inbucket/inbucket/blob/main/doc/config.md
[Configurator]: https://www.inbucket.org/configurator/
[CONTRIBUTING.md]: https://github.com/inbucket/inbucket/blob/main/CONTRIBUTING.md
[Development Quickstart]: https://github.com/inbucket/inbucket/wiki/Development-Quickstart
[Docker Image]: https://inbucket.org/packages/docker.html
[Elm]: https://elm-lang.org/
[From Source]: https://www.inbucket.org/installation/from-source.html
[Go]: https://golang.org/
[Go API docs]: https://pkg.go.dev/github.com/inbucket/inbucket/pkg/rest/client
[Homebrew]: http://brew.sh/
[Homebrew Tap]: https://github.com/inbucket/homebrew-inbucket
[Inbucket Website]: https://www.inbucket.org/
[Issues List]: https://github.com/inbucket/inbucket/issues?state=open
[Node.js]: https://nodejs.org/en/

53
cmd/client/list.go Normal file
View File

@@ -0,0 +1,53 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/google/subcommands"
"github.com/inbucket/inbucket/v3/pkg/rest/client"
)
type listCmd struct{}
func (*listCmd) Name() string {
return "list"
}
func (*listCmd) Synopsis() string {
return "list contents of mailbox"
}
func (*listCmd) Usage() string {
return `list <mailbox>:
list message IDs in mailbox
`
}
func (l *listCmd) SetFlags(f *flag.FlagSet) {}
func (l *listCmd) Execute(
ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
mailbox := f.Arg(0)
if mailbox == "" {
return usage("mailbox required")
}
// Setup rest client
c, err := client.New(baseURL())
if err != nil {
return fatal("Couldn't build client", err)
}
// Get list
headers, err := c.ListMailboxWithContext(ctx, mailbox)
if err != nil {
return fatal("REST call failed", err)
}
for _, h := range headers {
fmt.Println(h.ID)
}
return subcommands.ExitSuccess
}

84
cmd/client/main.go Normal file
View File

@@ -0,0 +1,84 @@
// Package main implements a command line client for the Inbucket REST API
package main
import (
"context"
"flag"
"fmt"
"net"
"os"
"regexp"
"strconv"
"github.com/google/subcommands"
)
var host = flag.String("host", "localhost", "host/IP of Inbucket server")
var port = flag.Uint("port", 9000, "HTTP port of Inbucket server")
// Allow subcommands to accept regular expressions as flags
type regexFlag struct {
*regexp.Regexp
}
func (r *regexFlag) Defined() bool {
return r.Regexp != nil
}
func (r *regexFlag) Set(pattern string) error {
if pattern == "" {
r.Regexp = nil
return nil
}
re, err := regexp.Compile(pattern)
if err != nil {
return err
}
r.Regexp = re
return nil
}
func (r *regexFlag) String() string {
if r.Regexp == nil {
return ""
}
return r.Regexp.String()
}
// regexFlag must implement flag.Value
var _ flag.Value = &regexFlag{}
func main() {
// Important top-level flags
subcommands.ImportantFlag("host")
subcommands.ImportantFlag("port")
// Setup standard helpers
subcommands.Register(subcommands.HelpCommand(), "")
subcommands.Register(subcommands.FlagsCommand(), "")
subcommands.Register(subcommands.CommandsCommand(), "")
// Setup my commands
subcommands.Register(&listCmd{}, "")
subcommands.Register(&matchCmd{}, "")
subcommands.Register(&mboxCmd{}, "")
// Parse and execute
flag.Parse()
ctx := context.Background()
os.Exit(int(subcommands.Execute(ctx)))
}
func baseURL() string {
return "http://%s" + net.JoinHostPort(*host, strconv.FormatUint(uint64(*port), 10))
}
func fatal(msg string, err error) subcommands.ExitStatus {
fmt.Fprintf(os.Stderr, "%s: %v\n", msg, err)
return subcommands.ExitFailure
}
func usage(msg string) subcommands.ExitStatus {
fmt.Fprintln(os.Stderr, msg)
return subcommands.ExitUsageError
}

171
cmd/client/match.go Normal file
View File

@@ -0,0 +1,171 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"net/mail"
"os"
"time"
"github.com/google/subcommands"
"github.com/inbucket/inbucket/v3/pkg/rest/client"
)
type matchCmd struct {
output string
outFunc func(ctx context.Context, headers []*client.MessageHeader) error
delete bool
// match criteria
from regexFlag
subject regexFlag
to regexFlag
maxAge time.Duration
}
func (*matchCmd) Name() string {
return "match"
}
func (*matchCmd) Synopsis() string {
return "output messages matching criteria"
}
func (*matchCmd) Usage() string {
return `match [flags] <mailbox>:
output messages matching all specified criteria
exit status will be 1 if no matches were found, otherwise 0
`
}
func (m *matchCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&m.output, "output", "id", "output format: id, json, or mbox")
f.BoolVar(&m.delete, "delete", false, "delete matched messages after output")
f.Var(&m.from, "from", "From header matching regexp (address, not name)")
f.Var(&m.subject, "subject", "Subject header matching regexp")
f.Var(&m.to, "to", "To header matching regexp (must match 1+ to address)")
f.DurationVar(
&m.maxAge, "maxage", 0,
"Matches must have been received in this time frame (ex: \"10s\", \"5m\")")
}
func (m *matchCmd) Execute(
ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
mailbox := f.Arg(0)
if mailbox == "" {
return usage("mailbox required")
}
// Select output function
switch m.output {
case "id":
m.outFunc = outputID
case "json":
m.outFunc = outputJSON
case "mbox":
m.outFunc = outputMbox
default:
return usage("unknown output type: " + m.output)
}
// Setup REST client
c, err := client.New(baseURL())
if err != nil {
return fatal("Couldn't build client", err)
}
// Get list
headers, err := c.ListMailboxWithContext(ctx, mailbox)
if err != nil {
return fatal("List REST call failed", err)
}
// Find matches
matches := make([]*client.MessageHeader, 0, len(headers))
for _, h := range headers {
if m.match(h) {
matches = append(matches, h)
}
}
// Return error status if no matches
if len(matches) == 0 {
return subcommands.ExitFailure
}
// Output matches
err = m.outFunc(ctx, matches)
if err != nil {
return fatal("Error", err)
}
// Optionally, delete matches
if m.delete {
for _, h := range matches {
err = h.DeleteWithContext(ctx)
if err != nil {
return fatal("Delete REST call failed", err)
}
}
}
return subcommands.ExitSuccess
}
// match returns true if header matches all defined criteria
func (m *matchCmd) match(header *client.MessageHeader) bool {
if m.maxAge > 0 {
if time.Since(header.Date) > m.maxAge {
return false
}
}
if m.subject.Defined() {
if !m.subject.MatchString(header.Subject) {
return false
}
}
if m.from.Defined() {
from := header.From
addr, err := mail.ParseAddress(from)
if err == nil {
// Parsed successfully
from = addr.Address
}
if !m.from.MatchString(from) {
return false
}
}
if m.to.Defined() {
match := false
for _, to := range header.To {
addr, err := mail.ParseAddress(to)
if err == nil {
// Parsed successfully
to = addr.Address
}
if m.to.MatchString(to) {
match = true
break
}
}
if !match {
return false
}
}
return true
}
func outputID(_ context.Context, headers []*client.MessageHeader) error {
for _, h := range headers {
fmt.Println(h.ID)
}
return nil
}
func outputJSON(_ context.Context, headers []*client.MessageHeader) error {
jsonEncoder := json.NewEncoder(os.Stdout)
jsonEncoder.SetEscapeHTML(false)
jsonEncoder.SetIndent("", " ")
return jsonEncoder.Encode(headers)
}

88
cmd/client/mbox.go Normal file
View File

@@ -0,0 +1,88 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"github.com/google/subcommands"
"github.com/inbucket/inbucket/v3/pkg/rest/client"
)
type mboxCmd struct {
delete bool
}
func (*mboxCmd) Name() string {
return "mbox"
}
func (*mboxCmd) Synopsis() string {
return "output mailbox in mbox format"
}
func (*mboxCmd) Usage() string {
return `mbox [flags] <mailbox>:
output mailbox in mbox format
`
}
func (m *mboxCmd) SetFlags(f *flag.FlagSet) {
f.BoolVar(&m.delete, "delete", false, "delete messages after output")
}
func (m *mboxCmd) Execute(
ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
mailbox := f.Arg(0)
if mailbox == "" {
return usage("mailbox required")
}
// Setup REST client
c, err := client.New(baseURL())
if err != nil {
return fatal("Couldn't build client", err)
}
// Get list
headers, err := c.ListMailboxWithContext(ctx, mailbox)
if err != nil {
return fatal("List REST call failed", err)
}
err = outputMbox(ctx, headers)
if err != nil {
return fatal("Error", err)
}
// Optionally, delete retrieved messages
if m.delete {
for _, h := range headers {
err = h.DeleteWithContext(ctx)
if err != nil {
return fatal("Delete REST call failed", err)
}
}
}
return subcommands.ExitSuccess
}
// outputMbox renders messages in mbox format.
// It is also used by match subcommand.
func outputMbox(ctx context.Context, headers []*client.MessageHeader) error {
for _, h := range headers {
source, err := h.GetSourceWithContext(ctx)
if err != nil {
return fmt.Errorf("get source REST failed: %v", err)
}
fmt.Printf("From %s\n", h.From)
// TODO Escape "From " in message bodies with >
if _, err := source.WriteTo(os.Stdout); err != nil {
return err
}
fmt.Println()
}
return nil
}

231
cmd/inbucket/main.go Normal file
View File

@@ -0,0 +1,231 @@
// main is the inbucket daemon launcher
package main
import (
"bufio"
"context"
"expvar"
"flag"
"fmt"
"io"
"os"
"os/signal"
"runtime"
"syscall"
"time"
"github.com/inbucket/inbucket/v3/pkg/config"
"github.com/inbucket/inbucket/v3/pkg/server"
"github.com/inbucket/inbucket/v3/pkg/storage"
"github.com/inbucket/inbucket/v3/pkg/storage/file"
"github.com/inbucket/inbucket/v3/pkg/storage/mem"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var (
// version contains the build version number, populated during linking.
version = "undefined"
// date contains the build date, populated during linking.
date = "undefined"
)
func init() {
// Server uptime for status page.
startTime := expvar.NewInt("startMillis")
startTime.Set(time.Now().UnixNano() / 1000000)
// Goroutine count for status page.
expvar.Publish("goroutines", expvar.Func(func() any {
return runtime.NumGoroutine()
}))
// Register storage implementations.
storage.Constructors["file"] = file.New
storage.Constructors["memory"] = mem.New
}
func main() {
// Command line flags.
help := flag.Bool("help", false, "Displays help on flags and env variables.")
versionflag := flag.Bool("version", false, "Displays version.")
pidfile := flag.String("pidfile", "", "Write our PID into the specified file.")
logfile := flag.String("logfile", "stderr", "Write out log into the specified file.")
logjson := flag.Bool("logjson", false, "Logs are written in JSON format.")
netdebug := flag.Bool("netdebug", false, "Dump SMTP & POP3 network traffic to stdout.")
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "Usage: inbucket [options]")
flag.PrintDefaults()
}
flag.Parse()
if *help {
flag.Usage()
fmt.Fprintln(os.Stderr, "")
config.Usage()
return
}
if *versionflag {
fmt.Fprintln(os.Stdout, version)
return
}
// Process configuration.
config.Version = version
config.BuildDate = date
conf, err := config.Process()
if err != nil {
fmt.Fprintf(os.Stderr, "Configuration error: %v\n", err)
os.Exit(1)
}
if *netdebug {
conf.POP3.Debug = true
conf.SMTP.Debug = true
}
// Logger setup.
closeLog, err := openLog(conf.LogLevel, *logfile, *logjson)
if err != nil {
fmt.Fprintf(os.Stderr, "Log error: %v\n", err)
os.Exit(1)
}
startupLog := log.With().Str("phase", "startup").Logger()
// Setup signal handler.
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
// Initialize logging.
startupLog.Info().Str("version", config.Version).Str("buildDate", config.BuildDate).
Msg("Inbucket starting")
// Write pidfile if requested.
if *pidfile != "" {
pidf, err := os.Create(*pidfile)
if err != nil {
startupLog.Fatal().Err(err).Str("path", *pidfile).Msg("Failed to create pidfile")
}
fmt.Fprintf(pidf, "%v\n", os.Getpid())
if err := pidf.Close(); err != nil {
startupLog.Fatal().Err(err).Str("path", *pidfile).Msg("Failed to close pidfile")
}
}
// Configure and start internal services.
svcCtx, svcCancel := context.WithCancel(context.Background())
services, err := server.FullAssembly(conf)
if err != nil {
startupLog.Fatal().Err(err).Msg("Fatal error during startup")
removePIDFile(*pidfile)
}
services.Start(svcCtx, func() {
startupLog.Debug().Msg("All services report ready")
})
// Loop forever waiting for signals or shutdown channel.
signalLoop:
for {
select {
case sig := <-sigChan:
switch sig {
case syscall.SIGINT:
// Shutdown requested
log.Info().Str("phase", "shutdown").Str("signal", "SIGINT").
Msg("Received SIGINT, shutting down")
svcCancel()
break signalLoop
case syscall.SIGTERM:
// Shutdown requested
log.Info().Str("phase", "shutdown").Str("signal", "SIGTERM").
Msg("Received SIGTERM, shutting down")
svcCancel()
break signalLoop
}
case <-services.Notify():
log.Info().Str("phase", "shutdown").Msg("Shutting down due to service failure")
svcCancel()
break signalLoop
}
}
// Wait for active connections to finish.
go timedExit(*pidfile)
log.Debug().Str("phase", "shutdown").Msg("Draining SMTP connections")
services.SMTPServer.Drain()
log.Debug().Str("phase", "shutdown").Msg("Draining POP3 connections")
services.POP3Server.Drain()
log.Debug().Str("phase", "shutdown").Msg("Checking retention scanner is stopped")
services.RetentionScanner.Join()
removePIDFile(*pidfile)
closeLog()
}
// openLog configures zerolog output, returns func to close logfile.
func openLog(level string, logfile string, json bool) (closeLog func(), err error) {
switch level {
case "debug":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
case "info":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case "warn":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case "error":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
default:
return nil, fmt.Errorf("log level %q not one of: debug, info, warn, error", level)
}
closeLog = func() {}
var w io.Writer
color := runtime.GOOS != "windows"
switch logfile {
case "stderr":
w = os.Stderr
case "stdout":
w = os.Stdout
default:
logf, err := os.OpenFile(logfile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
return nil, err
}
bw := bufio.NewWriter(logf)
w = bw
color = false
closeLog = func() {
_ = bw.Flush()
_ = logf.Close()
}
}
w = zerolog.SyncWriter(w)
if json {
log.Logger = log.Output(w)
return closeLog, nil
}
log.Logger = log.Output(zerolog.ConsoleWriter{
Out: w,
NoColor: !color,
})
return closeLog, nil
}
// removePIDFile removes the PID file if created.
func removePIDFile(pidfile string) {
if pidfile != "" {
if err := os.Remove(pidfile); err != nil {
log.Error().Str("phase", "shutdown").Err(err).Str("path", pidfile).
Msg("Failed to remove pidfile")
}
}
}
// timedExit is called as a goroutine during shutdown, it will force an exit after 15 seconds.
func timedExit(pidfile string) {
time.Sleep(15 * time.Second)
removePIDFile(pidfile)
log.Error().Str("phase", "shutdown").Msg("Clean shutdown took too long, forcing exit")
os.Exit(0)
}

View File

@@ -1,409 +0,0 @@
package config
import (
"container/list"
"fmt"
"net"
"os"
"strings"
"github.com/robfig/config"
)
// SMTPConfig contains the SMTP server configuration - not using pointers
// so that we can pass around copies of the object safely.
type SMTPConfig struct {
IP4address net.IP
IP4port int
Domain string
DomainNoStore string
MaxRecipients int
MaxIdleSeconds int
MaxMessageBytes int
StoreMessages bool
}
// POP3Config contains the POP3 server configuration
type POP3Config struct {
IP4address net.IP
IP4port int
Domain string
MaxIdleSeconds int
}
// WebConfig contains the HTTP server configuration
type WebConfig struct {
IP4address net.IP
IP4port int
TemplateDir string
TemplateCache bool
PublicDir string
GreetingFile string
CookieAuthKey string
}
// DataStoreConfig contains the mail store configuration
type DataStoreConfig struct {
Path string
RetentionMinutes int
RetentionSleep int
MailboxMsgCap int
}
var (
// Version of this build, set by main
Version = ""
// BuildDate for this build, set by main
BuildDate = ""
// Config is our global robfig/config object
Config *config.Config
// Parsed specific configs
smtpConfig *SMTPConfig
pop3Config *POP3Config
webConfig *WebConfig
dataStoreConfig *DataStoreConfig
)
// GetSMTPConfig returns a copy of the SmtpConfig object
func GetSMTPConfig() SMTPConfig {
return *smtpConfig
}
// GetPOP3Config returns a copy of the Pop3Config object
func GetPOP3Config() POP3Config {
return *pop3Config
}
// GetWebConfig returns a copy of the WebConfig object
func GetWebConfig() WebConfig {
return *webConfig
}
// GetDataStoreConfig returns a copy of the DataStoreConfig object
func GetDataStoreConfig() DataStoreConfig {
return *dataStoreConfig
}
// LoadConfig loads the specified configuration file into inbucket.Config
// and performs validations on it.
func LoadConfig(filename string) error {
var err error
Config, err = config.ReadDefault(filename)
if err != nil {
return err
}
messages := list.New()
// Validate sections
requireSection(messages, "logging")
requireSection(messages, "smtp")
requireSection(messages, "pop3")
requireSection(messages, "web")
requireSection(messages, "datastore")
if messages.Len() > 0 {
fmt.Fprintln(os.Stderr, "Error(s) validating configuration:")
for e := messages.Front(); e != nil; e = e.Next() {
fmt.Fprintln(os.Stderr, " -", e.Value.(string))
}
return fmt.Errorf("Failed to validate configuration")
}
// Validate options
requireOption(messages, "logging", "level")
requireOption(messages, "smtp", "ip4.address")
requireOption(messages, "smtp", "ip4.port")
requireOption(messages, "smtp", "domain")
requireOption(messages, "smtp", "max.recipients")
requireOption(messages, "smtp", "max.idle.seconds")
requireOption(messages, "smtp", "max.message.bytes")
requireOption(messages, "smtp", "store.messages")
requireOption(messages, "pop3", "ip4.address")
requireOption(messages, "pop3", "ip4.port")
requireOption(messages, "pop3", "domain")
requireOption(messages, "pop3", "max.idle.seconds")
requireOption(messages, "web", "ip4.address")
requireOption(messages, "web", "ip4.port")
requireOption(messages, "web", "template.dir")
requireOption(messages, "web", "template.cache")
requireOption(messages, "web", "public.dir")
requireOption(messages, "datastore", "path")
requireOption(messages, "datastore", "retention.minutes")
requireOption(messages, "datastore", "retention.sleep.millis")
requireOption(messages, "datastore", "mailbox.message.cap")
// Return error if validations failed
if messages.Len() > 0 {
fmt.Fprintln(os.Stderr, "Error(s) validating configuration:")
for e := messages.Front(); e != nil; e = e.Next() {
fmt.Fprintln(os.Stderr, " -", e.Value.(string))
}
return fmt.Errorf("Failed to validate configuration")
}
if err = parseSMTPConfig(); err != nil {
return err
}
if err = parsePOP3Config(); err != nil {
return err
}
if err = parseWebConfig(); err != nil {
return err
}
if err = parseDataStoreConfig(); err != nil {
return err
}
return nil
}
// parseLoggingConfig trying to catch config errors early
func parseLoggingConfig() error {
section := "logging"
option := "level"
str, err := Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
switch strings.ToUpper(str) {
case "TRACE", "INFO", "WARN", "ERROR":
default:
return fmt.Errorf("Invalid value provided for [%v]%v: '%v'", section, option, str)
}
return nil
}
// parseSMTPConfig trying to catch config errors early
func parseSMTPConfig() error {
smtpConfig = new(SMTPConfig)
section := "smtp"
// Parse IP4 address only, error on IP6.
option := "ip4.address"
str, err := Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
addr := net.ParseIP(str)
if addr == nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, str)
}
addr = addr.To4()
if addr == nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v' not IPv4!", section, option, str)
}
smtpConfig.IP4address = addr
option = "ip4.port"
smtpConfig.IP4port, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "domain"
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
smtpConfig.Domain = str
option = "domain.nostore"
if Config.HasOption(section, option) {
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
smtpConfig.DomainNoStore = str
}
option = "max.recipients"
smtpConfig.MaxRecipients, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "max.idle.seconds"
smtpConfig.MaxIdleSeconds, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "max.message.bytes"
smtpConfig.MaxMessageBytes, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "store.messages"
flag, err := Config.Bool(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
smtpConfig.StoreMessages = flag
return nil
}
// parsePOP3Config trying to catch config errors early
func parsePOP3Config() error {
pop3Config = new(POP3Config)
section := "pop3"
// Parse IP4 address only, error on IP6.
option := "ip4.address"
str, err := Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
addr := net.ParseIP(str)
if addr == nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, str)
}
addr = addr.To4()
if addr == nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v' not IPv4!", section, option, str)
}
pop3Config.IP4address = addr
option = "ip4.port"
pop3Config.IP4port, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "domain"
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
pop3Config.Domain = str
option = "max.idle.seconds"
pop3Config.MaxIdleSeconds, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
return nil
}
// parseWebConfig trying to catch config errors early
func parseWebConfig() error {
webConfig = new(WebConfig)
section := "web"
// Parse IP4 address only, error on IP6.
option := "ip4.address"
str, err := Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
addr := net.ParseIP(str)
if addr == nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, str)
}
addr = addr.To4()
if addr == nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v' not IPv4!", section, option, str)
}
webConfig.IP4address = addr
option = "ip4.port"
webConfig.IP4port, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "template.dir"
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
webConfig.TemplateDir = str
option = "template.cache"
flag, err := Config.Bool(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
webConfig.TemplateCache = flag
option = "public.dir"
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
webConfig.PublicDir = str
option = "greeting.file"
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
webConfig.GreetingFile = str
option = "cookie.auth.key"
if Config.HasOption(section, option) {
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
webConfig.CookieAuthKey = str
}
return nil
}
// parseDataStoreConfig trying to catch config errors early
func parseDataStoreConfig() error {
dataStoreConfig = new(DataStoreConfig)
section := "datastore"
option := "path"
str, err := Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
dataStoreConfig.Path = str
option = "retention.minutes"
dataStoreConfig.RetentionMinutes, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "retention.sleep.millis"
dataStoreConfig.RetentionSleep, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "mailbox.message.cap"
dataStoreConfig.MailboxMsgCap, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
return nil
}
// requireSection checks that a [section] is defined in the configuration file,
// appending a message if not.
func requireSection(messages *list.List, section string) {
if !Config.HasSection(section) {
messages.PushBack(fmt.Sprintf("Config section [%v] is required", section))
}
}
// requireOption checks that 'option' is defined in [section] of the config file,
// appending a message if not.
func requireOption(messages *list.List, section string, option string) {
if !Config.HasOption(section, option) {
messages.PushBack(fmt.Sprintf("Config option '%v' is required in section [%v]", option, section))
}
}

489
doc/config.md Normal file
View File

@@ -0,0 +1,489 @@
# Inbucket Configuration
Inbucket is configured via environment variables. Most options have a
reasonable default, but it is likely you will need to change some to suite your
desired use cases.
Running `inbucket -help` will yield a condensed summary of the environment
variables it supports:
KEY DEFAULT DESCRIPTION
INBUCKET_LOGLEVEL info debug, info, warn, or error
INBUCKET_LUA_PATH inbucket.lua Lua script path
INBUCKET_MAILBOXNAMING local Use local, full, or domain addressing
INBUCKET_SMTP_ADDR 0.0.0.0:2500 SMTP server IP4 host:port
INBUCKET_SMTP_DOMAIN inbucket HELO domain
INBUCKET_SMTP_MAXRECIPIENTS 200 Maximum RCPT TO per message
INBUCKET_SMTP_MAXMESSAGEBYTES 10240000 Maximum message size
INBUCKET_SMTP_DEFAULTACCEPT true Accept all mail by default?
INBUCKET_SMTP_ACCEPTDOMAINS Domains to accept mail for
INBUCKET_SMTP_REJECTDOMAINS Domains to reject mail for
INBUCKET_SMTP_REJECTORIGINDOMAINS Domains to reject mail from
INBUCKET_SMTP_DEFAULTSTORE true Store all mail by default?
INBUCKET_SMTP_STOREDOMAINS Domains to store mail for
INBUCKET_SMTP_DISCARDDOMAINS Domains to discard mail for
INBUCKET_SMTP_TIMEOUT 300s Idle network timeout
INBUCKET_SMTP_TLSENABLED false Enable STARTTLS option
INBUCKET_SMTP_TLSPRIVKEY cert.key X509 Private Key file for TLS Support
INBUCKET_SMTP_TLSCERT cert.crt X509 Public Certificate file for TLS Support
INBUCKET_POP3_ADDR 0.0.0.0:1100 POP3 server IP4 host:port
INBUCKET_POP3_DOMAIN inbucket HELLO domain
INBUCKET_POP3_TIMEOUT 600s Idle network timeout
INBUCKET_WEB_ADDR 0.0.0.0:9000 Web server IP4 host:port
INBUCKET_WEB_BASEPATH Base path prefix for UI and API URLs
INBUCKET_WEB_UIDIR ui/dist User interface dir
INBUCKET_WEB_GREETINGFILE ui/greeting.html Home page greeting HTML
INBUCKET_WEB_MONITORVISIBLE true Show monitor tab in UI?
INBUCKET_WEB_MONITORHISTORY 30 Monitor remembered messages
INBUCKET_WEB_PPROF false Expose profiling tools on /debug/pprof
INBUCKET_STORAGE_TYPE memory Storage impl: file or memory
INBUCKET_STORAGE_PARAMS Storage impl parameters, see docs.
INBUCKET_STORAGE_RETENTIONPERIOD 24h Duration to retain messages
INBUCKET_STORAGE_RETENTIONSLEEP 50ms Duration to sleep between mailboxes
INBUCKET_STORAGE_MAILBOXMSGCAP 500 Maximum messages per mailbox
The following documentation will describe each of these in more detail.
## Global
### Log Level
`INBUCKET_LOGLEVEL`
This setting controls the verbosity of log output. A small desktop installation
should probably select `info`, but a busy shared installation would be better
off with `warn` or `error`.
- Default: `info`
- Values: one of `debug`, `info`, `warn`, or `error`
### Lua Script
`INBUCKET_LUA_PATH`
This is the path to the (optional) Inbucket Lua script. If the specified file
is present, Inbucket will load it during startup. Ignored if the file is not
found, or the setting is empty.
- Default: `inbucket.lua`
### Mailbox Naming
`INBUCKET_MAILBOXNAMING`
The mailbox naming setting determines the name of a mailbox for an incoming
message, and thus where it must be retrieved from later.
#### `local` ensures the domain is removed, such that:
- `james@inbucket.org` is stored in `james`
- `james+spam@inbucket.org` is stored in `james`
#### `full` retains the domain as part of the name, such that:
- `james@inbucket.org` is stored in `james@inbucket.org`
- `james+spam@inbucket.org` is stored in `james@inbucket.org`
Prior to the addition of the mailbox naming setting, Inbucket always operated in
local mode. Regardless of this setting, the `+` wildcard/extension is not
incorporated into the mailbox name.
#### `domain` ensures the local-part is removed, such that:
- `james@inbucket.org` is stored in `inbucket.org`
- `matt@inbucket.org` is stored in `inbucket.org`
- `matt@noinbucket.com` is stored in `notinbucket.com`
- Default: `local`
- Values: one of `local` or `full` or `domain`
## SMTP
### Address and Port
`INBUCKET_SMTP_ADDR`
The IPv4 address and TCP port number the SMTP server should listen on, separated
by a colon. Some operating systems may prevent Inbucket from listening on port
25 without escalated privileges. Using an IP address of 0.0.0.0 will cause
Inbucket to listen on all available network interfaces.
- Default: `0.0.0.0:2500`
### Greeting Domain
`INBUCKET_SMTP_DOMAIN`
The domain used in the SMTP greeting:
220 domain Inbucket SMTP ready
Most SMTP clients appear to ignore this value.
- Default: `inbucket`
### Maximum Recipients
`INBUCKET_SMTP_MAXRECIPIENTS`
Maximum number of recipients allowed (SMTP `RCPT TO` phase). If you are testing
a mailing list server, you may need to increase this value. For comparison, the
Postfix SMTP server uses a default of 1000, it would be unwise to exceed this.
- Default: `200`
### Maximum Message Size
`INBUCKET_SMTP_MAXMESSAGEBYTES`
Maximum allowable size of a message (including headers) in bytes. Messages
exceeding this size will be rejected during the SMTP `DATA` phase.
- Default: `10240000` (10MB)
### Default Recipient Accept Policy
`INBUCKET_SMTP_DEFAULTACCEPT`
If true, Inbucket will accept mail to any domain unless present in the reject
domains list. If false, recipients will be rejected unless their domain is
present in the accept domains list.
- Default: `true`
- Values: `true` or `false`
### Accepted Recipient Domain List
`INBUCKET_SMTP_ACCEPTDOMAINS`
List of domains to accept mail for when `INBUCKET_SMTP_DEFAULTACCEPT` is false;
has no effect when true.
- Default: None
- Values: Comma separated list of recipient domains
- Example: `localhost,mysite.org`
### Rejected Recipient Domain List
`INBUCKET_SMTP_REJECTDOMAINS`
List of domains to reject mail for when `INBUCKET_SMTP_DEFAULTACCEPT` is true;
has no effect when false.
- Default: None
- Values: Comma separated list of recipient domains
- Example: `reject.com,gmail.com`
### Rejected Origin Domain List
`INBUCKET_SMTP_REJECTORIGINDOMAINS`
List of domains to reject mail from. This list is enforced regardless of the
`INBUCKET_SMTP_DEFAULTACCEPT` value.
Enforcement takes place during evalation of the `MAIL FROM` SMTP command, the
origin domain is extracted from the address presented and compared against the
list. It does not take email headers into account.
- Default: None
- Values: Comma separated list of origin domains
- Example: `reject.com,gmail.com`
### Default Recipient Store Policy
`INBUCKET_SMTP_DEFAULTSTORE`
If true, Inbucket will store mail sent to any domain unless present in the
discard domains list. If false, messages will be discarded unless their domain
is present in the store domains list.
- Default: `true`
- Values: `true` or `false`
### Stored Recipient Domain List
`INBUCKET_SMTP_STOREDOMAINS`
List of domains to store mail for when `INBUCKET_SMTP_DEFAULTSTORE` is false;
has no effect when true.
- Default: None
- Values: Comma separated list of recipient domains
- Example: `localhost,mysite.org`
### Discarded Recipient Domain List
`INBUCKET_SMTP_DISCARDDOMAINS`
Mail sent to these domains will not be stored by Inbucket. This is helpful if
you are load or soak testing a service, and do not plan to inspect the resulting
emails. Messages sent to a domain other than this will be stored normally.
Only has an effect when `INBUCKET_SMTP_DEFAULTSTORE` is true.
- Default: None
- Values: Comma separated list of recipient domains
- Example: `recycle.com,loadtest.org`
### Network Idle Timeout
`INBUCKET_SMTP_TIMEOUT`
Delay before closing an idle SMTP connection. The SMTP RFC recommends 300
seconds. Consider reducing this *significantly* if you plan to expose Inbucket
to the public internet.
- Default: `300s`
- Values: Duration ending in `s` for seconds, `m` for minutes
### TLS Support Availability
`INBUCKET_SMTP_TLSENABLED`
Enable the STARTTLS option for opportunistic TLS support
- Default: `false`
- Values: `true` or `false`
### TLS Private Key File
`INBUCKET_SMTP_TLSPRIVKEY`
Specify the x509 Private key file to be used for TLS negotiation.
This option is only valid when INBUCKET_SMTP_TLSENABLED is enabled.
- Default: `cert.key`
- Values: filename or path to private key
- Example: `server.privkey`
### TLS Public Certificate File
`INBUCKET_SMTP_TLSCERT`
Specify the x509 Certificate file to be used for TLS negotiation.
This option is only valid when INBUCKET_SMTP_TLSENABLED is enabled.
- Default: `cert.crt`
- Values: filename or path to the certificate key
- Example: `server.crt`
## POP3
### Address and Port
`INBUCKET_POP3_ADDR`
The IPv4 address and TCP port number the POP3 server should listen on, separated
by a colon. Some operating systems may prevent Inbucket from listening on port
110 without escalated privileges. Using an IP address of 0.0.0.0 will cause
Inbucket to listen on all available network interfaces.
- Default: `0.0.0.0:1100`
### Greeting Domain
`INBUCKET_POP3_DOMAIN`
The domain used in the POP3 greeting:
+OK Inbucket POP3 server ready <26641.1522000423@domain>
Most POP3 clients appear to ignore this value.
- Default: `inbucket`
### Network Idle Timeout
`INBUCKET_POP3_TIMEOUT`
Delay before closing an idle POP3 connection. The POP3 RFC recommends 600
seconds. Consider reducing this *significantly* if you plan to expose Inbucket
to the public internet.
- Default: `600s`
- Values: Duration ending in `s` for seconds, `m` for minutes
## Web
### Address and Port
`INBUCKET_WEB_ADDR`
The IPv4 address and TCP port number the HTTP server should listen on, separated
by a colon. Some operating systems may prevent Inbucket from listening on port
80 without escalated privileges. Using an IP address of 0.0.0.0 will cause
Inbucket to listen on all available network interfaces.
- Default: `0.0.0.0:9000`
### Base Path
`INBUCKET_WEB_BASEPATH`
Base path prefix for UI and API URLs. This option is used when you wish to
root all Inbucket URLs to a specific path when placing it behind a
reverse-proxy.
For example, setting the base path to `prefix` will move:
- the Inbucket status page from `/status` to `/prefix/status`,
- Bob's mailbox from `/m/bob` to `/prefix/m/bob`, and
- the REST API from `/api/v1/*` to `/prefix/api/v1/*`.
*Note:* This setting will not work correctly when running Inbucket via the npm
development server.
- Default: None
### UI Directory
`INBUCKET_WEB_UIDIR`
This directory contains the templates and static assets for the web user
interface. You will need to change this if the current working directory
doesn't contain the `ui` directory at startup.
Inbucket will load templates from the `templates` sub-directory, and serve
static assets from the `static` sub-directory.
- Default: `ui/dist`
- Values: Operating system specific path syntax
### Greeting HTML File
`INBUCKET_WEB_GREETINGFILE`
The content of the greeting file will be injected into the front page of
Inbucket. It can be used to instruct users on how to send mail into your
Inbucket installation, as well as link to REST documentation, etc.
- Default: `ui/greeting.html`
### Monitor Visible
`INBUCKET_WEB_MONITORVISIBLE`
If true, the Monitor tab will be available, allowing users to observe all
messages received by Inbucket as they arrive. Disabling the monitor facilitates
security through obscurity.
This setting has no impact on the availability of the underlying WebSocket,
which may be used by other parts of the Inbucket interface or continuous
integration tests.
- Default: `true`
- Values: `true` or `false`
### Monitor History
`INBUCKET_WEB_MONITORHISTORY`
The number of messages to remember on the *server* for new Monitor clients.
Does not impact the amount of *new* messages displayed by the Monitor.
Increasing this has no appreciable impact on memory use, but may slow down the
Monitor user interface.
This setting has the same effect on the amount of messages available via
WebSocket.
Setting to 0 will disable the monitor, but will probably break new mail
notifications in the web interface when I finally get around to implementing
them.
- Default: `30`
- Values: Integer greater than or equal to 0
### Performance Profiling & Debug Tools
`INBUCKET_WEB_PPROF`
If true, Go's pprof package will be installed to the `/debug/pprof` URI. This
exposes detailed memory and CPU performance data for debugging Inbucket. If you
enable this option, please make sure it is not exposed to the public internet,
as its use can significantly impact performance.
For example usage, see https://golang.org/pkg/net/http/pprof/
- Default: `false`
- Values: `true` or `false`
## Storage
### Type
`INBUCKET_STORAGE_TYPE`
Selects the storage implementation to use. Currently Inbucket supports two:
- `file`: stores messages as individual files in a nested directory structure
based on the hash of the mailbox name. Each mailbox also includes an index
file to speed up enumeration of the mailbox contents.
- `memory`: stores messages in RAM, they will be lost if Inbucket is restarted,
or crashes, etc.
File storage is recommended for larger/shared installations. Memory is better
suited to desktop or continuous integration test use cases.
- Default: `memory`
- Values: `file` or `memory`
### Parameters
`INBUCKET_STORAGE_PARAMS`
Parameters specific to the storage type selected. Formatted as a comma
separated list of key:value pairs.
- Default: None
- Examples: `maxkb:10240` or `path:/tmp/inbucket`
#### `file` type parameters
- `path`: Operating system specific path to the directory where mail should be
stored. `$` characters will be replaced with `:` in the final path value,
allowing Windows drive letters, i.e. `D$\inbucket`.
#### `memory` type parameters
- `maxkb`: Maximum size of the mail store in kilobytes. The oldest messages in
the store will be deleted to enforce the limit. In-memory storage has some
overhead, for now it is recommended to set this to half the total amount of
memory you are willing to allocate to Inbucket.
### Retention Period
`INBUCKET_STORAGE_RETENTIONPERIOD`
If set, Inbucket will scan the contents of its mail store once per minute,
removing messages older than this. This will be enforced regardless of the type
of storage configured.
- Default: `24h`
- Values: Duration ending in `m` for minutes, `h` for hours. Should be
significantly longer than one minute, or `0` to disable.
### Retention Sleep
`INBUCKET_STORAGE_RETENTIONSLEEP`
Duration to sleep between scanning each mailbox for expired messages.
Increasing this number will reduce disk thrashing, but extend the length of time
required to complete a scan of the entire mail store.
This delay is still enforced for memory stores, but could be reduced from the
default. Setting to `0` may degrade performance of HTTP/SMTP/POP3 services.
- Default: `50ms`
- Values: Duration ending in `ms` for milliseconds, `s` for seconds
### Per Mailbox Message Cap
`INBUCKET_STORAGE_MAILBOXMSGCAP`
Maximum messages allowed in a single mailbox, exceeding this will cause older
messages to be deleted from the mailbox.
- Default: `500`
- Values: Positive integer, or `0` to disable

36
etc/dev-start.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/sh
# dev-start.sh
# description: Developer friendly Inbucket configuration
export INBUCKET_LOGLEVEL="debug"
#export INBUCKET_MAILBOXNAMING="domain"
export INBUCKET_SMTP_REJECTDOMAINS="bad-actors.local"
#export INBUCKET_SMTP_DEFAULTACCEPT="false"
export INBUCKET_SMTP_ACCEPTDOMAINS="good-actors.local"
export INBUCKET_SMTP_DISCARDDOMAINS="bitbucket.local"
#export INBUCKET_SMTP_DEFAULTSTORE="false"
export INBUCKET_SMTP_STOREDOMAINS="important.local"
export INBUCKET_WEB_TEMPLATECACHE="false"
export INBUCKET_WEB_COOKIEAUTHKEY="not-secret"
export INBUCKET_WEB_UIDIR="ui/dist"
#export INBUCKET_WEB_MONITORVISIBLE="false"
#export INBUCKET_WEB_BASEPATH="prefix"
export INBUCKET_STORAGE_TYPE="file"
export INBUCKET_STORAGE_PARAMS="path:/tmp/inbucket"
export INBUCKET_STORAGE_RETENTIONPERIOD="3h"
export INBUCKET_STORAGE_MAILBOXMSGCAP="300"
if ! test -x ./inbucket; then
echo "$PWD/inbucket not found/executable!" >&2
echo "Run this script from the inbucket root directory after running make." >&2
exit 1
fi
index="$INBUCKET_WEB_UIDIR/index.html"
if ! test -f "$index"; then
echo "$index does not exist!" >&2
echo "Run 'yarn build' from the 'ui' directory." >&2
exit 1
fi
exec ./inbucket $*

View File

@@ -1,113 +0,0 @@
# devel.conf
# Sample development configuration
#############################################################################
[DEFAULT]
# Not used directly, but is typically referenced below in %()s format.
install.dir=.
default.domain=inbucket.local
#############################################################################
[logging]
# Options from least to most verbose: ERROR, WARN, INFO, TRACE
level=TRACE
#############################################################################
[smtp]
# IPv4 address to listen for SMTP connections on.
ip4.address=0.0.0.0
# IPv4 port to listen for SMTP connections on.
ip4.port=2500
# used in SMTP greeting
domain=%(default.domain)s
# optional: mail sent to accounts at this domain will not be stored,
# for mixed use (content and load testing)
domain.nostore=bitbucket.local
# Maximum number of RCPT TO: addresses we allow from clients, the SMTP
# RFC recommends this be at least 100.
max.recipients=100
# How long we allow a network connection to be idle before hanging up on the
# client, SMTP RFC recommends at least 5 minutes (300 seconds).
max.idle.seconds=30
# Maximum allowable size of message body in bytes (including attachments)
max.message.bytes=20480000
# Should we place messages into the datastore, or just throw them away
# (for load testing): true or false
store.messages=true
#############################################################################
[pop3]
# IPv4 address to listen for POP3 connections on.
ip4.address=0.0.0.0
# IPv4 port to listen for POP3 connections on.
ip4.port=1100
# used in POP3 greeting
domain=%(default.domain)s
# How long we allow a network connection to be idle before hanging up on the
# client, POP3 RFC requires at least 10 minutes (600 seconds).
max.idle.seconds=600
#############################################################################
[web]
# IPv4 address to serve HTTP web interface on
ip4.address=0.0.0.0
# IPv4 port to serve HTTP web interface on
ip4.port=9000
# Name of web theme to use
theme=bootstrap
# Path to the selected themes template files
template.dir=%(install.dir)s/themes/%(theme)s/templates
# Should we cache parsed templates (set to false during theme dev)
template.cache=false
# Path to the selected themes public (static) files
public.dir=%(install.dir)s/themes/%(theme)s/public
# Path to the greeting HTML displayed on front page, can be moved out of
# installation dir for customization
greeting.file=%(install.dir)s/themes/greeting.html
# Key used to sign session cookie data so that it cannot be tampered with.
# If this is left unset, Inbucket will generate a random key at startup
# and previous sessions will be invalidated.
cookie.auth.key=secret-inbucket-session-cookie-key
#############################################################################
[datastore]
# Path to the datastore, mail will be written into subdirectories
path=/tmp/inbucket
# How many minutes after receipt should a message be stored until it's
# automatically purged. To retain messages until manually deleted, set this
# to 0
retention.minutes=0
# How many milliseconds to sleep after purging messages from a mailbox.
# This should help reduce disk I/O when there are a large number of messages
# to purge.
retention.sleep.millis=100
# Maximum number of messages we will store in a single mailbox. If this
# number is exceeded, the oldest message in the box will be deleted each
# time a new message is received for it.
mailbox.message.cap=100

View File

@@ -1,7 +1,9 @@
<h1>Welcome to Inbucket</h1>
<p>Inbucket is an email testing service; it will accept email for any email
address and make it available to view without a password.</p>
<p>To view email for a particular address, enter the username portion
<p>To view messages for a particular address, enter the username portion
of the address into the box on the upper right and click <em>View</em>.</p>
<p>This instance of Inbucket is running inside of a <a
@@ -11,5 +13,7 @@ of 300 messages per mailbox - the oldest messages will be deleted to stay under
that limit.</p>
<p>Messages addressed to any recipient in the <code>@bitbucket.local</code>
domain will be accepted but not written to disk. Use this domain for load or
soak testing your application.</p>
domain will be accepted, but immediately <b>discarded</b> without being written
to disk. Use this domain for load or soak testing your application. Inbucket
will retain mail for any other domain by default, i.e.
<code>@inbucket.local</code>.</p>

View File

@@ -1,115 +0,0 @@
# inbucket.conf
# Configuration for Inbucket inside of Docker
#
# These should be reasonable defaults for a production install of Inbucket
#############################################################################
[DEFAULT]
# Not used directly, but is typically referenced below in %()s format.
install.dir=/opt/inbucket
default.domain=inbucket.local
#############################################################################
[logging]
# Options from least to most verbose: ERROR, WARN, INFO, TRACE
level=INFO
#############################################################################
[smtp]
# IPv4 address to listen for SMTP connections on.
ip4.address=0.0.0.0
# IPv4 port to listen for SMTP connections on.
ip4.port=10025
# used in SMTP greeting
domain=%(default.domain)s
# optional: mail sent to accounts at this domain will not be stored,
# for mixed use (content and load testing)
domain.nostore=bitbucket.local
# Maximum number of RCPT TO: addresses we allow from clients, the SMTP
# RFC recommends this be at least 100.
max.recipients=100
# How long we allow a network connection to be idle before hanging up on the
# client, SMTP RFC recommends at least 5 minutes (300 seconds).
max.idle.seconds=300
# Maximum allowable size of message body in bytes (including attachments)
max.message.bytes=2048000
# Should we place messages into the datastore, or just throw them away
# (for load testing): true or false
store.messages=true
#############################################################################
[pop3]
# IPv4 address to listen for POP3 connections on.
ip4.address=0.0.0.0
# IPv4 port to listen for POP3 connections on.
ip4.port=10110
# used in POP3 greeting
domain=%(default.domain)s
# How long we allow a network connection to be idle before hanging up on the
# client, POP3 RFC requires at least 10 minutes (600 seconds).
max.idle.seconds=600
#############################################################################
[web]
# IPv4 address to serve HTTP web interface on
ip4.address=0.0.0.0
# IPv4 port to serve HTTP web interface on
ip4.port=10080
# Name of web theme to use
theme=bootstrap
# Path to the selected themes template files
template.dir=%(install.dir)s/themes/%(theme)s/templates
# Should we cache parsed templates (set to false during theme dev)
template.cache=true
# Path to the selected themes public (static) files
public.dir=%(install.dir)s/themes/%(theme)s/public
# Path to the greeting HTML displayed on front page, can be moved out of
# installation dir for customization
greeting.file=/con/configuration/greeting.html
# Key used to sign session cookie data so that it cannot be tampered with.
# If this is left unset, Inbucket will generate a random key at startup
# and previous sessions will be invalidated.
#cookie.auth.key=secret-inbucket-session-cookie-key
#############################################################################
[datastore]
# Path to the datastore, mail will be written into subdirectories
path=/con/data
# How many minutes after receipt should a message be stored until it's
# automatically purged. To retain messages until manually deleted, set this
# to 0
retention.minutes=4320
# How many milliseconds to sleep after purging messages from a mailbox.
# This should help reduce disk I/O when there are a large number of messages
# to purge.
retention.sleep.millis=100
# Maximum number of messages we will store in a single mailbox. If this
# number is exceeded, the oldest message in the box will be deleted each
# time a new message is received for it.
mailbox.message.cap=300

View File

@@ -2,8 +2,9 @@
# start-inbucket.sh
# description: start inbucket (runs within a docker container)
INBUCKET_HOME="/opt/inbucket"
CONF_SOURCE="$INBUCKET_HOME/defaults"
CONF_TARGET="/con/configuration"
CONF_TARGET="/config"
set -eo pipefail
@@ -18,7 +19,6 @@ install_default_config() {
fi
}
install_default_config "inbucket.conf"
install_default_config "greeting.html"
exec "$INBUCKET_HOME/bin/inbucket" $*

View File

@@ -3,7 +3,7 @@
# description: Launch Inbucket's docker image
# Docker Image Tag
IMAGE="jhillyerd/inbucket"
IMAGE="inbucket/inbucket:edge"
# Ports exposed on host:
PORT_HTTP=9000
@@ -12,9 +12,9 @@ PORT_POP3=1100
# Volumes exposed on host:
VOL_CONFIG="/tmp/inbucket/config"
VOL_DATA="/tmp/inbucket/data"
VOL_DATA="/tmp/inbucket/storage"
set -eo pipefail
set -e
main() {
local run_opts=""
@@ -25,6 +25,9 @@ main() {
usage
exit
;;
-b)
build
;;
-r)
reset
;;
@@ -38,25 +41,34 @@ main() {
esac
done
set -x
docker run $run_opts \
-p $PORT_HTTP:10080 \
-p $PORT_SMTP:10025 \
-p $PORT_POP3:10110 \
-v "$VOL_CONFIG:/con/configuration" \
-v "$VOL_DATA:/con/data" \
-p $PORT_HTTP:9000 \
-p $PORT_SMTP:2500 \
-p $PORT_POP3:1100 \
-v "$VOL_CONFIG:/config" \
-v "$VOL_DATA:/storage" \
"$IMAGE"
}
usage() {
echo "$0 [options]" 2>&1
echo " -b build - build image before starting" 2>&1
echo " -d detach - detach and print container ID" 2>&1
echo " -r reset - purge config and data before startup" 2>&1
echo " -h help - print this message" 2>&1
}
build() {
echo "Building $IMAGE"
docker build . -t "$IMAGE"
echo
}
reset() {
/bin/rm -rf "$VOL_CONFIG"
/bin/rm -rf "$VOL_DATA"
rm -rf "$VOL_CONFIG"
rm -rf "$VOL_DATA"
}
main $*

View File

@@ -1,48 +0,0 @@
#!/bin/sh
# install.sh
# description: Build, test, and install Inbucket. Should be executed inside a Docker container.
set -eo pipefail
installdir="$INBUCKET_HOME"
srcdir="$INBUCKET_SRC"
bindir="$installdir/bin"
defaultsdir="$installdir/defaults"
contextdir="/con/context"
echo "### Installing OS Build Dependencies"
apk add --no-cache --virtual .build-deps git
# Setup
export GOBIN="$bindir"
builddate="$(date -Iseconds)"
cd "$srcdir"
go clean
# Build
echo "### Fetching Dependencies"
go get -t -v ./...
echo "### Testing Inbucket"
go test ./...
echo "### Building Inbucket"
go build -o inbucket -ldflags "-X 'main.BUILDDATE=$builddate'" -v .
echo "### Installing Inbucket"
set -x
mkdir -p "$bindir"
install inbucket "$bindir"
mkdir -p "$contextdir"
install etc/docker/defaults/start-inbucket.sh "$contextdir"
cp -r themes "$installdir/"
mkdir -p "$defaultsdir"
cp etc/docker/defaults/inbucket.conf "$defaultsdir"
cp etc/docker/defaults/greeting.html "$defaultsdir"
set +x
echo "### Removing OS Build Dependencies"
apk del .build-deps
echo "### Removing $GOPATH"
rm -rf "$GOPATH"

View File

@@ -1,113 +0,0 @@
# inbucket.conf
# Sample inbucket configuration
#############################################################################
[DEFAULT]
# Not used directly, but is typically referenced below in %()s format.
install.dir=.
default.domain=inbucket.local
#############################################################################
[logging]
# Options from least to most verbose: ERROR, WARN, INFO, TRACE
level=INFO
#############################################################################
[smtp]
# IPv4 address to listen for SMTP connections on.
ip4.address=0.0.0.0
# IPv4 port to listen for SMTP connections on.
ip4.port=2500
# used in SMTP greeting
domain=%(default.domain)s
# optional: mail sent to accounts at this domain will not be stored,
# for mixed use (content and load testing)
#domain.nostore=bitbucket.local
# Maximum number of RCPT TO: addresses we allow from clients, the SMTP
# RFC recommends this be at least 100.
max.recipients=100
# How long we allow a network connection to be idle before hanging up on the
# client, SMTP RFC recommends at least 5 minutes (300 seconds).
max.idle.seconds=300
# Maximum allowable size of message body in bytes (including attachments)
max.message.bytes=2048000
# Should we place messages into the datastore, or just throw them away
# (for load testing): true or false
store.messages=true
#############################################################################
[pop3]
# IPv4 address to listen for POP3 connections on.
ip4.address=0.0.0.0
# IPv4 port to listen for POP3 connections on.
ip4.port=1100
# used in POP3 greeting
domain=%(default.domain)s
# How long we allow a network connection to be idle before hanging up on the
# client, POP3 RFC requires at least 10 minutes (600 seconds).
max.idle.seconds=600
#############################################################################
[web]
# IPv4 address to serve HTTP web interface on
ip4.address=0.0.0.0
# IPv4 port to serve HTTP web interface on
ip4.port=9000
# Name of web theme to use
theme=bootstrap
# Path to the selected themes template files
template.dir=%(install.dir)s/themes/%(theme)s/templates
# Should we cache parsed templates (set to false during theme dev)
template.cache=true
# Path to the selected themes public (static) files
public.dir=%(install.dir)s/themes/%(theme)s/public
# Path to the greeting HTML displayed on front page, can be moved out of
# installation dir for customization
greeting.file=%(install.dir)s/themes/greeting.html
# Key used to sign session cookie data so that it cannot be tampered with.
# If this is left unset, Inbucket will generate a random key at startup
# and previous sessions will be invalidated.
#cookie.auth.key=secret-inbucket-session-cookie-key
#############################################################################
[datastore]
# Path to the datastore, mail will be written into subdirectories
path=/tmp/inbucket
# How many minutes after receipt should a message be stored until it's
# automatically purged. To retain messages until manually deleted, set this
# to 0
retention.minutes=240
# How many milliseconds to sleep after purging messages from a mailbox.
# This should help reduce disk I/O when there are a large number of messages
# to purge.
retention.sleep.millis=100
# Maximum number of messages we will store in a single mailbox. If this
# number is exceeded, the oldest message in the box will be deleted each
# time a new message is received for it.
mailbox.message.cap=500

View File

@@ -0,0 +1,33 @@
[Unit]
Description=Inbucket Disposable Email Service
After=network.target
[Service]
Type=simple
User=daemon
Group=daemon
PermissionsStartOnly=true
Environment=INBUCKET_LOGLEVEL=warn
Environment=INBUCKET_SMTP_ADDR=0.0.0.0:2500
Environment=INBUCKET_POP3_ADDR=0.0.0.0:1100
Environment=INBUCKET_WEB_ADDR=0.0.0.0:9000
Environment=INBUCKET_WEB_UIDIR=/usr/share/inbucket/ui
Environment=INBUCKET_WEB_GREETINGFILE=/etc/inbucket/greeting.html
Environment=INBUCKET_STORAGE_TYPE=file
Environment=INBUCKET_STORAGE_PARAMS=path:/var/inbucket
# Uncomment line below to use low numbered ports
#ExecStartPre=/sbin/setcap 'cap_net_bind_service=+ep' /usr/bin/inbucket
ExecStartPre=/bin/mkdir -p /var/inbucket
ExecStartPre=/bin/chown daemon:daemon /var/inbucket
ExecStart=/usr/bin/inbucket
# Give SMTP connections time to drain
TimeoutStopSec=20
KillMode=mixed
[Install]
WantedBy=multi-user.target

View File

@@ -1,3 +0,0 @@
Please see the RedHat installation guide on our website:
http://www.inbucket.org/installation/redhat.html

View File

@@ -1,17 +0,0 @@
# Inbucket reverse proxy, Apache will forward requests from port 80
# to Inbucket's built in web server on port 9000
#
# Replace SERVERFQDN with your servers fully qualified domain name
<VirtualHost *:80>
ServerName SERVERFQDN
ProxyRequests off
<Proxy *>
Order allow,deny
Allow from all
</Proxy>
RewriteRule ^/$ http://SERVERFQDN:9000
ProxyPass / http://SERVERFQDN:9000/
ProxyPassReverse / http://SERVERFQDN:9000/
</VirtualHost>

View File

@@ -1,117 +0,0 @@
#!/bin/sh
#
# inbucket Inbucket email testing service
#
# chkconfig: 2345 80 30
# description: Inbucket is a disposable email service for testing email
# functionality of other applications.
# processname: inbucket
# pidfile: /var/run/inbucket/inbucket.pid
### BEGIN INIT INFO
# Provides: Inbucket service
# Required-Start: $local_fs $network $remote_fs
# Required-Stop: $local_fs $network $remote_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: start and stop inbucket
# Description: Inbucket is a disposable email service for testing email
# functionality of other applications.
# moves mail from one machine to another.
### END INIT INFO
# Source function library.
. /etc/rc.d/init.d/functions
# Source networking configuration.
. /etc/sysconfig/network
RETVAL=0
program=/opt/inbucket/inbucket
prog=${program##*/}
config=/etc/opt/inbucket.conf
runas=inbucket
lockfile=/var/lock/subsys/$prog
pidfile=/var/run/$prog/$prog.pid
logfile=/var/log/$prog.log
conf_check() {
[ -x $program ] || exit 5
[ -f $config ] || exit 6
}
perms_check() {
mkdir -p /var/run/$prog
chown $runas: /var/run/$prog
touch $logfile
chown $runas: $logfile
# Allow bind to ports under 1024
setcap 'cap_net_bind_service=+ep' $program
}
start() {
[ "$EUID" != "0" ] && exit 4
# Check that networking is up.
[ ${NETWORKING} = "no" ] && exit 1
# Check config sanity
conf_check
perms_check
# Start daemon
echo -n $"Starting $prog: "
daemon --user $runas --pidfile $pidfile $program \
-pidfile $pidfile -logfile $logfile $config \&
RETVAL=$?
[ $RETVAL -eq 0 ] && touch $lockfile
echo
return $RETVAL
}
stop() {
[ "$EUID" != "0" ] && exit 4
conf_check
# Stop daemon
echo -n $"Shutting down $prog: "
killproc -p "$pidfile" -d 15 "$program"
RETVAL=$?
[ $RETVAL -eq 0 ] && rm -f $lockfile $pidfile
echo
return $RETVAL
}
reload() {
[ "$EUID" != "0" ] && exit 4
echo -n $"Reloading $prog: "
killproc -p "$pidfile" "$program" -HUP
RETVAL=$?
echo
return $RETVAL
}
# See how we were called.
case "$1" in
start)
[ -e $lockfile ] && exit 0
start
;;
stop)
[ -e $lockfile ] || exit 0
stop
;;
reload)
[ -e $lockfile ] || exit 0
reload
;;
restart|force-reload)
stop
start
;;
status)
status -p $pidfile -l $(basename $lockfile) $prog
;;
*)
echo $"Usage: $0 {start|stop|restart|status}"
exit 2
esac
exit $?

View File

@@ -1,8 +0,0 @@
/var/log/inbucket.log {
missingok
notifempty
create 0644 inbucket inbucket
postrotate
[ -e /etc/init.d/inbucket ] && /etc/init.d/inbucket reload >/dev/null 2>&1 || true
endscript
}

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# rest-apiv1.sh
# description: Script to access Inbucket REST API version 1
@@ -103,7 +103,7 @@ main() {
esac
# Use jq to pretty-print if installed and we are expecting JSON output
if [ $pretty ] && [ $is_json ] && type -P jq; then
if [ $pretty ] && [ $is_json ] && type -P jq >/dev/null; then
curl -s $curl_opts -H "Accept: application/json" --noproxy "$API_HOST" -X "$method" "$url" | jq .
else
curl -s $curl_opts -H "Accept: application/json" --noproxy "$API_HOST" -X "$method" "$url"

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,31 @@
Date: %DATE%
To: %TO_ADDRESS%
From: %FROM_ADDRESS%
Subject: MIME Errors
Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet>
Content-Type: multipart/alternative; boundary="Enmime-Test-100"
--Enmime-Test-100
Content-Transfer-Encoding: 8bit
Content-Type: text/plain; charset=us-ascii
Using Unicode/UTF-8, you can write in emails and source code things such as
Mathematics and sciences:
∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i), ⎧⎡⎛┌─────┐⎞⎤⎫
⎪⎢⎜│a²+b³ ⎟⎥⎪
∀x∈: ⌈x⌉ = x⌋, α ∧ ¬β = ¬(¬α β), ⎪⎢⎜│───── ⎟⎥⎪
⎪⎢⎜⎷ c₈ ⎟⎥⎪
⊆ ℕ₀ ⊂ , ⎨⎢⎜ ⎟⎥⎬
⎪⎢⎜ ∞ ⎟⎥⎪
⊥ < a ≠ b ≡ c ≤ d ≪ ⇒ (⟦A⟧ ⇔ ⟪B⟫), ⎪⎢⎜ ⎲ ⎟⎥⎪
⎪⎢⎜ ⎳aⁱ-bⁱ⎟⎥⎪
2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm ⎩⎣⎝i=1 ⎠⎦⎭
Linguistics and dictionaries:
ði ıntəˈnæʃənəl fəˈnɛtık əsoʊsiˈeıʃn
Y [ˈʏpsilɔn], Yen [jɛn], Yoga [ˈjoːgɑ]
--Enmime-Test-100

View File

@@ -0,0 +1,43 @@
Subject: Inline attachment
From: %FROM_ADDRESS%
To: %TO_ADDRESS%
Message-ID: <1234@example.com>
Date: %DATE%
Content-Type: multipart/mixed; boundary=boundary1
--boundary1
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<!DOCTYPE html>
<html>
<head>
<title>Hello World HTML</title>
</head>
<body>
<h1 style=3D"color:red">Hello World</h1>
</body>
</html>
--boundary1
Content-Type: application/pdf; name=Hello-World.pdf
Content-Transfer-Encoding: base64
Content-Disposition: inline; name=Hello-World.pdf;
filename=Hello-World.pdf
JVBERi0xLjQKJcK1wrYKCjEgMCBvYmoKPDwvVGl0bGUoSGVsbG8gV29ybGQpL0F1dGhvcihBZHJp
dW0pPj4KZW5kb2JqCgoyIDAgb2JqCjw8L1R5cGUvQ2F0YWxvZy9QYWdlcyAzIDAgUj4+CmVuZG9i
agoKMyAwIG9iago8PC9UeXBlL1BhZ2VzL01lZGlhQm94WzAgMCA1OTUgODQyXS9SZXNvdXJjZXM8
PC9Gb250PDwvRjEgNCAwIFI+Pi9Qcm9jU2V0Wy9QREYvVGV4dF0+Pi9LaWRzWzUgMCBSXS9Db3Vu
dCAxPj4KZW5kb2JqCgo0IDAgb2JqCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1R5cGUxL0Jhc2VGb250
L0hlbHZldGljYS9FbmNvZGluZy9XaW5BbnNpRW5jb2Rpbmc+PgplbmRvYmoKCjUgMCBvYmoKPDwv
VHlwZS9QYWdlL1BhcmVudCAzIDAgUi9Db250ZW50cyA2IDAgUj4+CmVuZG9iagoKNiAwIG9iago8
PC9MZW5ndGggNTEvRmlsdGVyL0ZsYXRlRGVjb2RlPj4Kc3RyZWFtCnic03czVDCxUAhJ43IK4TI3
UjA3MVMISeHS8EjNyclXCM8vyknRVAjJ4nIN4QIA3FcKuwplbmRzdHJlYW0KZW5kb2JqCgp4cmVm
CjAgNwowMDAwMDAwMDAwIDY1NTM2IGYgCjAwMDAwMDAwMTYgMDAwMDAgbiAKMDAwMDAwMDA3MSAw
MDAwMCBuIAowMDAwMDAwMTE3IDAwMDAwIG4gCjAwMDAwMDAyNDIgMDAwMDAgbiAKMDAwMDAwMDMz
MSAwMDAwMCBuIAowMDAwMDAwMzkwIDAwMDAwIG4gCgp0cmFpbGVyCjw8L1NpemUgNy9JbmZvIDEg
MCBSL1Jvb3QgMiAwIFI+PgpzdGFydHhyZWYKNTA5CiUlRU9GCg==
--boundary1--

View File

@@ -0,0 +1,393 @@
Date: %DATE%
To: %TO_ADDRESS%
From: %FROM_ADDRESS%
Subject: tutsplus responsive inlined CSS
MIME-Version: 1.0
Content-Type: text/html; charset="UTF-8"
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]-->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
table {border-collapse: collapse !important;}
</style>
<![endif]-->
</head>
<body style="margin-top:0 !important;margin-bottom:0 !important;margin-right:0 !important;margin-left:0 !important;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;background-color:#ffffff;" >
<center class="wrapper" style="width:100%;table-layout:fixed;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;" >
<div class="webkit" style="max-width:600px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;" >
<!--[if (gte mso 9)|(IE)]>
<table width="600" align="center" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<table class="outer" align="center" style="border-spacing:0;font-family:sans-serif;color:#333333;Margin:0 auto;width:100%;max-width:600px;" >
<tr>
<td class="full-width-image" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<img src="http://www.inbucket.org/email-assets/responsive/header.jpg" width="600" alt="" style="border-width:0;width:100%;max-width:600px;height:auto;" />
</td>
</tr>
<tr>
<td class="one-column" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner contents" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;width:100%;text-align:left;" >
<p class="h1" style="Margin:0;font-weight:bold;font-size:14px;Margin-bottom:10px;" >Lorem ipsum dolor sit amet</p>
<p style="Margin:0;font-size:14px;Margin-bottom:10px;" >
Compare to:
<a href="http://tutsplus.github.io/creating-a-future-proof-responsive-email-without-media-queries/index.html" style="color:#ee6a56;text-decoration:underline;" >
tutsplus sample</a>
</p>
<p style="Margin:0;font-size:14px;Margin-bottom:10px;" >Copyright (c) 2015, Envato Tuts+<br/>
All rights reserved.</p>
<p style="Margin:0;font-size:14px;Margin-bottom:10px;" >Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:</p>
<ul>
<li>Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.</li>
<li>Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.</li>
</ul>
<p style="Margin:0;font-size:14px;Margin-bottom:10px;" >THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="two-column" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;text-align:center;font-size:0;" >
<!--[if (gte mso 9)|(IE)]>
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td width="50%" valign="top" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column" style="width:100%;max-width:300px;display:inline-block;vertical-align:top;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;" >
<table class="contents" style="border-spacing:0;font-family:sans-serif;color:#333333;width:100%;font-size:14px;text-align:left;" >
<tr>
<td style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<img src="http://www.inbucket.org/email-assets/responsive/two-column-01.jpg" width="280" alt="" style="border-width:0;width:100%;max-width:280px;height:auto;" />
</td>
</tr>
<tr>
<td class="text" style="padding-bottom:0;padding-right:0;padding-left:0;padding-top:10px;" >
Maecenas sed ante pellentesque, posuere leo id, eleifend dolor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td><td width="50%" valign="top" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column" style="width:100%;max-width:300px;display:inline-block;vertical-align:top;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;" >
<table class="contents" style="border-spacing:0;font-family:sans-serif;color:#333333;width:100%;font-size:14px;text-align:left;" >
<tr>
<td style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<img src="http://www.inbucket.org/email-assets/responsive/two-column-02.jpg" width="280" alt="" style="border-width:0;width:100%;max-width:280px;height:auto;" />
</td>
</tr>
<tr>
<td class="text" style="padding-bottom:0;padding-right:0;padding-left:0;padding-top:10px;" >
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas sed ante pellentesque, posuere leo id, eleifend dolor.
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<tr>
<td class="three-column" style="padding-right:0;padding-left:0;text-align:center;font-size:0;padding-top:10px;padding-bottom:10px;" >
<!--[if (gte mso 9)|(IE)]>
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td width="200" valign="top" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column" style="width:100%;max-width:200px;display:inline-block;vertical-align:top;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;" >
<table class="contents" style="border-spacing:0;font-family:sans-serif;color:#333333;width:100%;font-size:14px;text-align:center;" >
<tr>
<td style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<img src="http://www.inbucket.org/email-assets/responsive/three-column-01.jpg" width="180" alt="" style="border-width:0;width:100%;max-width:180px;height:auto;" />
</td>
</tr>
<tr>
<td class="text" style="padding-bottom:0;padding-right:0;padding-left:0;padding-top:10px;" >
Scelerisque congue eros eu posuere. Praesent in felis ut velit pretium lobortis rhoncus ut erat.
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td><td width="200" valign="top" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column" style="width:100%;max-width:200px;display:inline-block;vertical-align:top;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;" >
<table class="contents" style="border-spacing:0;font-family:sans-serif;color:#333333;width:100%;font-size:14px;text-align:center;" >
<tr>
<td style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<img src="http://www.inbucket.org/email-assets/responsive/three-column-02.jpg" width="180" alt="" style="border-width:0;width:100%;max-width:180px;height:auto;" />
</td>
</tr>
<tr>
<td class="text" style="padding-bottom:0;padding-right:0;padding-left:0;padding-top:10px;" >
Maecenas sed ante pellentesque, posuere leo id, eleifend dolor.
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td><td width="200" valign="top" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column" style="width:100%;max-width:200px;display:inline-block;vertical-align:top;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;" >
<table class="contents" style="border-spacing:0;font-family:sans-serif;color:#333333;width:100%;font-size:14px;text-align:center;" >
<tr>
<td style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<img src="http://www.inbucket.org/email-assets/responsive/three-column-03.jpg" width="180" alt="" style="border-width:0;width:100%;max-width:180px;height:auto;" />
</td>
</tr>
<tr>
<td class="text" style="padding-bottom:0;padding-right:0;padding-left:0;padding-top:10px;" >
Praesent laoreet malesuada cursus. Maecenas scelerisque congue eros eu posuere.
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<tr>
<td class="three-column" style="padding-right:0;padding-left:0;text-align:center;font-size:0;padding-top:10px;padding-bottom:10px;" >
<!--[if (gte mso 9)|(IE)]>
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td width="200" valign="top" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column" style="width:100%;max-width:200px;display:inline-block;vertical-align:top;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner contents" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;width:100%;font-size:14px;text-align:center;" >
<p class="h2" style="Margin:0;font-size:18px;font-weight:bold;Margin-bottom:12px;" >Fashion</p>
<p style="Margin:0;" >Class eleifend aptent taciti sociosqu ad litora torquent conubia</p>
<p style="Margin:0;" ><a href="#" style="color:#ee6a56;text-decoration:underline;" >Read requirements</a></p>
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td><td width="200" valign="top" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column" style="width:100%;max-width:200px;display:inline-block;vertical-align:top;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner contents" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;width:100%;font-size:14px;text-align:center;" >
<p class="h2" style="Margin:0;font-size:18px;font-weight:bold;Margin-bottom:12px;" >Photography</p>
<p style="Margin:0;" >Maecenas sed ante pellentesque, posuere leo id, eleifend dolor</p>
<p style="Margin:0;" ><a href="#" style="color:#ee6a56;text-decoration:underline;" >See examples</a></p>
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td><td width="200" valign="top" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column" style="width:100%;max-width:200px;display:inline-block;vertical-align:top;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner contents" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;width:100%;font-size:14px;text-align:center;" >
<p class="h2" style="Margin:0;font-size:18px;font-weight:bold;Margin-bottom:12px;" >Design</p>
<p style="Margin:0;" >Class aptent taciti sociosqu eleifend ad litora per conubia nostra</p>
<p style="Margin:0;" ><a href="#" style="color:#ee6a56;text-decoration:underline;" >See the winners</a></p>
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
<tr>
<td width="200" valign="top" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column" style="width:100%;max-width:200px;display:inline-block;vertical-align:top;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner contents" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;width:100%;font-size:14px;text-align:center;" >
<p class="h2" style="Margin:0;font-size:18px;font-weight:bold;Margin-bottom:12px;" >Cooking</p>
<p style="Margin:0;" >Class aptent taciti eleifend sociosqu ad litora torquent conubia</p>
<p style="Margin:0;" ><a href="#" style="color:#ee6a56;text-decoration:underline;" >Read recipes</a></p>
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td><td width="200" valign="top" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column" style="width:100%;max-width:200px;display:inline-block;vertical-align:top;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner contents" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;width:100%;font-size:14px;text-align:center;" >
<p class="h2" style="Margin:0;font-size:18px;font-weight:bold;Margin-bottom:12px;" >Woodworking</p>
<p style="Margin:0;" >Maecenas sed ante pellentesque, posuere leo id, eleifend dolor</p>
<p style="Margin:0;" ><a href="#" style="color:#ee6a56;text-decoration:underline;" >See examples</a></p>
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td><td width="200" valign="top" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column" style="width:100%;max-width:200px;display:inline-block;vertical-align:top;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner contents" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;width:100%;font-size:14px;text-align:center;" >
<p class="h2" style="Margin:0;font-size:18px;font-weight:bold;Margin-bottom:12px;" >Craft</p>
<p style="Margin:0;" >Class aptent taciti sociosqu ad eleifend litora per conubia nostra</p>
<p style="Margin:0;" ><a href="#" style="color:#ee6a56;text-decoration:underline;" >Vote now</a></p>
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<tr>
<td class="left-sidebar" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;text-align:center;font-size:0;" >
<!--[if (gte mso 9)|(IE)]>
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td width="100" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column left" style="width:100%;display:inline-block;vertical-align:middle;max-width:100px;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;" >
<img src="http://www.inbucket.org/email-assets/responsive/sidebar-01.jpg" width="80" alt="" style="border-width:0;" />
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td><td width="500" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column right" style="width:100%;display:inline-block;vertical-align:middle;max-width:500px;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner contents" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;width:100%;font-size:14px;text-align:center;" >
Praesent laoreet malesuada cursus. Maecenas scelerisque congue eros eu posuere. Praesent in felis ut velit pretium lobortis rhoncus ut erat. <a href="#" style="text-decoration:underline;color:#85ab70;" >Read&nbsp;on</a>
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<tr>
<td class="right-sidebar" dir="rtl" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;text-align:center;font-size:0;" >
<!--[if (gte mso 9)|(IE)]>
<table width="100%" dir="rtl" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td width="100" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column left" dir="ltr" style="width:100%;display:inline-block;vertical-align:middle;max-width:100px;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner contents" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;width:100%;font-size:14px;text-align:center;" >
<img src="http://www.inbucket.org/email-assets/responsive/sidebar-02.jpg" width="80" alt="" style="border-width:0;" />
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td><td width="500" style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
<![endif]-->
<div class="column right" dir="ltr" style="width:100%;display:inline-block;vertical-align:middle;max-width:500px;" >
<table width="100%" style="border-spacing:0;font-family:sans-serif;color:#333333;" >
<tr>
<td class="inner contents" style="padding-top:10px;padding-bottom:10px;padding-right:10px;padding-left:10px;width:100%;font-size:14px;text-align:center;" >
Maecenas sed ante pellentesque, posuere leo id, eleifend dolor. Class aptent taciti sociosqu ad litora torquent per conubia nostra. <a href="#" style="text-decoration:underline;color:#70bbd9;" >Per&nbsp;inceptos</a>
</td>
</tr>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</div>
</center>
</body>
</html>

View File

@@ -1,7 +1,7 @@
Date: %DATE%
To: %TO_ADDRESS%
From: %FROM_ADDRESS%
Subject: tutsplus responsive (broken images)
Subject: tutsplus responsive external CSS
MIME-Version: 1.0
Content-Type: text/html; charset="UTF-8"
@@ -14,7 +14,7 @@ Content-Type: text/html; charset="UTF-8"
<!--<![endif]-->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<link rel="stylesheet" type="text/css" href="styles.css" />
<link rel="stylesheet" type="text/css" href="http://www.inbucket.org/email-assets/responsive/styles.css" />
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
table {border-collapse: collapse;}
@@ -32,7 +32,7 @@ Content-Type: text/html; charset="UTF-8"
<table class="outer" align="center">
<tr>
<td class="full-width-image">
<img src="images/header.jpg" width="600" alt="" />
<img src="http://www.inbucket.org/email-assets/responsive/header.jpg" width="600" alt="" />
</td>
</tr>
<tr>
@@ -91,7 +91,7 @@ Content-Type: text/html; charset="UTF-8"
<table class="contents">
<tr>
<td>
<img src="images/two-column-01.jpg" width="280" alt="" />
<img src="http://www.inbucket.org/email-assets/responsive/two-column-01.jpg" width="280" alt="" />
</td>
</tr>
<tr>
@@ -114,7 +114,7 @@ Content-Type: text/html; charset="UTF-8"
<table class="contents">
<tr>
<td>
<img src="images/two-column-02.jpg" width="280" alt="" />
<img src="http://www.inbucket.org/email-assets/responsive/two-column-02.jpg" width="280" alt="" />
</td>
</tr>
<tr>
@@ -148,7 +148,7 @@ Content-Type: text/html; charset="UTF-8"
<table class="contents">
<tr>
<td>
<img src="images/three-column-01.jpg" width="180" alt="" />
<img src="http://www.inbucket.org/email-assets/responsive/three-column-01.jpg" width="180" alt="" />
</td>
</tr>
<tr>
@@ -171,7 +171,7 @@ Content-Type: text/html; charset="UTF-8"
<table class="contents">
<tr>
<td>
<img src="images/three-column-02.jpg" width="180" alt="" />
<img src="http://www.inbucket.org/email-assets/responsive/three-column-02.jpg" width="180" alt="" />
</td>
</tr>
<tr>
@@ -194,7 +194,7 @@ Content-Type: text/html; charset="UTF-8"
<table class="contents">
<tr>
<td>
<img src="images/three-column-03.jpg" width="180" alt="" />
<img src="http://www.inbucket.org/email-assets/responsive/three-column-03.jpg" width="180" alt="" />
</td>
</tr>
<tr>
@@ -323,7 +323,7 @@ Content-Type: text/html; charset="UTF-8"
<table width="100%">
<tr>
<td class="inner">
<img src="images/sidebar-01.jpg" width="80" alt="" />
<img src="http://www.inbucket.org/email-assets/responsive/sidebar-01.jpg" width="80" alt="" />
</td>
</tr>
</table>
@@ -358,7 +358,7 @@ Content-Type: text/html; charset="UTF-8"
<table width="100%">
<tr>
<td class="inner contents">
<img src="images/sidebar-02.jpg" width="80" alt="" />
<img src="http://www.inbucket.org/email-assets/responsive/sidebar-02.jpg" width="80" alt="" />
</td>
</tr>
</table>

View File

@@ -0,0 +1,10 @@
Date: %DATE%
To: %TO_ADDRESS%
From: %FROM_ADDRESS%
Subject: Swaks HTML
MIME-Version: 1.0
Content-Type: text/html; charset="UTF-8"
<p style="font-family: 'Courier New', Courier, monospace;">
This is a test of <b>HTML</b> at the <i>top</i> level.
</p>

View File

@@ -1,8 +1,9 @@
#!/bin/sh
#!/usr/bin/env bash
# run-tests.sh
# description: Generate test emails for Inbucket
set -eo pipefail
[ $TRACE ] && set -x
# We need to be in swaks-tests directory
cmdpath="$(dirname "$0")"
@@ -19,15 +20,20 @@ case "$1" in
;;
*)
to="$1"
shift
;;
esac
export SWAKS_OPT_server="127.0.0.1:2500"
export SWAKS_OPT_server="${SWAKS_OPT_server:-127.0.0.1:2500}"
export SWAKS_OPT_to="$to@inbucket.local"
# Basic test
swaks $* --h-Subject: "Swaks Plain Text" --body text.txt
# Multi-recipient test
swaks $* --to="$to@inbucket.local,alternate@inbucket.local" --h-Subject: "Swaks Multi-Recipient" \
--body text.txt
# HTML test
swaks $* --h-Subject: "Swaks HTML" --data mime-html.raw
@@ -35,7 +41,8 @@ swaks $* --h-Subject: "Swaks HTML" --data mime-html.raw
swaks $* --h-Subject: "Swaks Top Level HTML" --data nonmime-html.raw
# Attachment test
swaks $* --h-Subject: "Swaks Attachment" --attach-type image/png --attach favicon.png --body text.txt
swaks $* --h-Subject: "Swaks Attachment" --attach-type image/png --attach favicon.png \
--body text.txt
# Encoded subject line test
swaks $* --data utf8-subject.raw
@@ -46,5 +53,16 @@ swaks $* --data gmail.raw
# Outlook test
swaks $* --data outlook.raw
# Nonemime responsive HTML test
# Non-mime responsive HTML test
swaks $* --data nonmime-html-responsive.raw
swaks $* --data nonmime-html-inlined.raw
# Incorrect charset, malformed final boundary
swaks $* --data mime-errors.raw
# IP RCPT domain
swaks $* --to="swaks@[127.0.0.1]" --h-Subject: "IPv4 RCPT Address" --body text.txt
swaks $* --to="swaks@[IPv6:2001:db8:aaaa:1::100]" --h-Subject: "IPv6 RCPT Address" --body text.txt
# Inline attachment test
swaks $* --data mime-inline.raw

View File

@@ -1,6 +1,8 @@
Date: %DATE%
To: %TO_ADDRESS%
From: %FROM_ADDRESS%
To: %TO_ADDRESS%,
=?utf-8?B?VGVzdCBvZiDIh8myyqLIr8ihyarJtMqb?= <recipient@inbucket.org>
From: =?utf-8?q?X-=C3=A4=C3=A9=C3=9F_Y-=C3=A4=C3=A9=C3=9F?=
<fromuser@inbucket.org>
Subject: =?utf-8?B?VGVzdCBvZiDIh8myyqLIr8ihyarJtMqb?=
Thread-Topic: =?utf-8?B?VGVzdCBvZiDIh8myyqLIr8ihyarJtMqb?=
Thread-Index: Ac6+4nH7mOymA+1JRQyk2LQPe1bEcw==

View File

@@ -1,3 +0,0 @@
Please see the Ubuntu installation guide on our website:
http://www.inbucket.org/installation/ubuntu.html

View File

@@ -1,29 +0,0 @@
# inbucket - disposable email service
#
# Inbucket is an SMTP server with a web interface for testing application
# functionality
description "inbucket - disposable email service"
author "http://jhillyerd.github.com/inbucket"
start on (local-filesystems and net-device-up IFACE!=lo)
stop on runlevel [!2345]
env program=/opt/inbucket/inbucket
env config=/etc/opt/inbucket.conf
env logfile=/var/log/inbucket.log
env runas=inbucket
# Give SMTP connections time to drain
kill timeout 20
pre-start script
[ -x $program ]
[ -r $config ]
touch $logfile
chown $runas: $logfile
# Allow bind to ports under 1024
setcap 'cap_net_bind_service=+ep' $program
end script
exec start-stop-daemon --start --chuid $runas --exec $program -- -logfile $logfile $config

View File

@@ -1,8 +0,0 @@
/var/log/inbucket.log {
missingok
notifempty
create 0644 inbucket inbucket
postrotate
[ -x /sbin/reload ] && /sbin/reload inbucket >/dev/null 2>&1 || true
endscript
}

View File

@@ -1,113 +0,0 @@
# inbucket.conf
# Sample inbucket configuration
#############################################################################
[DEFAULT]
# Not used directly, but is typically referenced below in %()s format.
install.dir=/opt/inbucket
default.domain=inbucket.local
#############################################################################
[logging]
# Options from least to most verbose: ERROR, WARN, INFO, TRACE
level=INFO
#############################################################################
[smtp]
# IPv4 address to listen for SMTP connections on.
ip4.address=0.0.0.0
# IPv4 port to listen for SMTP connections on.
ip4.port=25
# used in SMTP greeting
domain=%(default.domain)s
# optional: mail sent to accounts at this domain will not be stored,
# for mixed use (content and load testing)
#domain.nostore=bitbucket.local
# Maximum number of RCPT TO: addresses we allow from clients, the SMTP
# RFC recommends this be at least 100.
max.recipients=100
# How long we allow a network connection to be idle before hanging up on the
# client, SMTP RFC recommends at least 5 minutes (300 seconds).
max.idle.seconds=300
# Maximum allowable size of message body in bytes (including attachments)
max.message.bytes=2048000
# Should we place messages into the datastore, or just throw them away
# (for load testing): true or false
store.messages=true
#############################################################################
[pop3]
# IPv4 address to listen for POP3 connections on.
ip4.address=0.0.0.0
# IPv4 port to listen for POP3 connections on.
ip4.port=110
# used in POP3 greeting
domain=%(default.domain)s
# How long we allow a network connection to be idle before hanging up on the
# client, POP3 RFC requires at least 10 minutes (600 seconds).
max.idle.seconds=600
#############################################################################
[web]
# IPv4 address to serve HTTP web interface on
ip4.address=0.0.0.0
# IPv4 port to serve HTTP web interface on
ip4.port=80
# Name of web theme to use
theme=bootstrap
# Path to the selected themes template files
template.dir=%(install.dir)s/themes/%(theme)s/templates
# Should we cache parsed templates (set to false during theme dev)
template.cache=true
# Path to the selected themes public (static) files
public.dir=%(install.dir)s/themes/%(theme)s/public
# Path to the greeting HTML displayed on front page, can be moved out of
# installation dir for customization
greeting.file=%(install.dir)s/themes/greeting.html
# Key used to sign session cookie data so that it cannot be tampered with.
# If this is left unset, Inbucket will generate a random key at startup
# and previous sessions will be invalidated.
#cookie.auth.key=secret-inbucket-session-cookie-key
#############################################################################
[datastore]
# Path to the datastore, mail will be written into subdirectories
path=/var/opt/inbucket
# How many minutes after receipt should a message be stored until it's
# automatically purged. To retain messages until manually deleted, set this
# to 0
retention.minutes=240
# How many milliseconds to sleep after purging messages from a mailbox.
# This should help reduce disk I/O when there are a large number of messages
# to purge.
retention.sleep.millis=100
# Maximum number of messages we will store in a single mailbox. If this
# number is exceeded, the oldest message in the box will be deleted each
# time a new message is received for it.
mailbox.message.cap=500

View File

@@ -1,113 +0,0 @@
# win-sample.conf
# Sample inbucket configuration for Windows
#############################################################################
[DEFAULT]
# Not used directly, but is typically referenced below in %()s format.
install.dir=.
default.domain=inbucket.local
#############################################################################
[logging]
# Options from least to most verbose: ERROR, WARN, INFO, TRACE
level=INFO
#############################################################################
[smtp]
# IPv4 address to listen for SMTP connections on.
ip4.address=0.0.0.0
# IPv4 port to listen for SMTP connections on.
ip4.port=2500
# used in SMTP greeting
domain=%(default.domain)s
# optional: mail sent to accounts at this domain will not be stored,
# for mixed use (content and load testing)
#domain.nostore=bitbucket.local
# Maximum number of RCPT TO: addresses we allow from clients, the SMTP
# RFC recommends this be at least 100.
max.recipients=100
# How long we allow a network connection to be idle before hanging up on the
# client, SMTP RFC recommends at least 5 minutes (300 seconds).
max.idle.seconds=300
# Maximum allowable size of message body in bytes (including attachments)
max.message.bytes=2048000
# Should we place messages into the datastore, or just throw them away
# (for load testing): true or false
store.messages=true
#############################################################################
[pop3]
# IPv4 address to listen for POP3 connections on.
ip4.address=0.0.0.0
# IPv4 port to listen for POP3 connections on.
ip4.port=1100
# used in POP3 greeting
domain=%(default.domain)s
# How long we allow a network connection to be idle before hanging up on the
# client, POP3 RFC requires at least 10 minutes (600 seconds).
max.idle.seconds=600
#############################################################################
[web]
# IPv4 address to serve HTTP web interface on
ip4.address=0.0.0.0
# IPv4 port to serve HTTP web interface on
ip4.port=9000
# Name of web theme to use
theme=bootstrap
# Path to the selected themes template files
template.dir=%(install.dir)s\themes\%(theme)s\templates
# Should we cache parsed templates (set to false during theme dev)
template.cache=true
# Path to the selected themes public (static) files
public.dir=%(install.dir)s\themes\%(theme)s\public
# Path to the greeting HTML displayed on front page, can be moved out of
# installation dir for customization
greeting.file=%(install.dir)s\themes\greeting.html
# Key used to sign session cookie data so that it cannot be tampered with.
# If this is left unset, Inbucket will generate a random key at startup
# and previous sessions will be invalidated.
#cookie.auth.key=secret-inbucket-session-cookie-key
#############################################################################
[datastore]
# Path to the datastore, mail will be written into subdirectories
path=.\inbucket-data
# How many minutes after receipt should a message be stored until it's
# automatically purged. To retain messages until manually deleted, set this
# to 0
retention.minutes=240
# How many milliseconds to sleep after purging messages from a mailbox.
# This should help reduce disk I/O when there are a large number of messages
# to purge.
retention.sleep.millis=100
# Maximum number of messages we will store in a single mailbox. If this
# number is exceeded, the oldest message in the box will be deleted each
# time a new message is received for it.
mailbox.message.cap=500

44
go.mod Normal file
View File

@@ -0,0 +1,44 @@
module github.com/inbucket/inbucket/v3
go 1.25.0
require (
github.com/cjoudrey/gluahttp v0.0.0-20201111170219-25003d9adfa9
github.com/cosmotek/loguago v1.0.0
github.com/google/subcommands v1.2.0
github.com/gorilla/css v1.0.1
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3
github.com/inbucket/gopher-json v0.2.0
github.com/jhillyerd/enmime/v2 v2.1.0
github.com/jhillyerd/goldiff v0.1.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/microcosm-cc/bluemonday v1.0.27
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.10.0
github.com/yuin/gopher-lua v1.1.1
golang.org/x/net v0.40.0
)
require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

93
go.sum Normal file
View File

@@ -0,0 +1,93 @@
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/cjoudrey/gluahttp v0.0.0-20201111170219-25003d9adfa9 h1:rdWOzitWlNYeUsXmz+IQfa9NkGEq3gA/qQ3mOEqBU6o=
github.com/cjoudrey/gluahttp v0.0.0-20201111170219-25003d9adfa9/go.mod h1:X97UjDTXp+7bayQSFZk2hPvCTmTZIicUjZQRtkwgAKY=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cosmotek/loguago v1.0.0 h1:cM6xoMPoIL1hRPicMenFNVohylundRIPz+OfpadJyY0=
github.com/cosmotek/loguago v1.0.0/go.mod h1:M/3wRiTLODLY6ufA9sVxOgSvnkYv53sYuDTQEqX0lZ4=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/inbucket/gopher-json v0.2.0 h1:v/luoFy5olitFhByVUGMZ3LmtcroRs9YHlyrBedz7EA=
github.com/inbucket/gopher-json v0.2.0/go.mod h1:1BK2XgU9y+ibiRkylJQeV44AV9DrO8dVsgOJ6vpqF3g=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime/v2 v2.1.0 h1:c8Qwi5Xq5EdtMN6byQWoZ/8I2RMTo6OJ7Xay+s1oPO0=
github.com/jhillyerd/enmime/v2 v2.1.0/go.mod h1:EJ74dcRbBcqHSP2TBu08XRoy6y3Yx0cevwb1YkGMEmQ=
github.com/jhillyerd/goldiff v0.1.0 h1:7JzKPKVwAg1GzrbnsToYzq3Y5+S7dXM4hgEYiOzaf4A=
github.com/jhillyerd/goldiff v0.1.0/go.mod h1:WeDal6DTqhbMhNkf5REzWCIvKl3JWs0Q9omZ/huIWAs=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 h1:noHsffKZsNfU38DwcXWEPldrTjIZ8FPNKx8mYMGnqjs=
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7/go.mod h1:bbMEM6aU1WDF1ErA5YJ0p91652pGv140gGw4Ww3RGp8=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,64 +0,0 @@
package httpd
import (
"fmt"
"html"
"html/template"
"regexp"
"strings"
"time"
"github.com/jhillyerd/inbucket/log"
)
// TemplateFuncs declares functions made available to all templates (including partials)
var TemplateFuncs = template.FuncMap{
"friendlyTime": FriendlyTime,
"reverse": Reverse,
"textToHtml": TextToHTML,
}
// From http://daringfireball.net/2010/07/improved_regex_for_matching_urls
var urlRE = regexp.MustCompile("(?i)\\b((?:[a-z][\\w-]+:(?:/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}/)(?:[^\\s()<>]+|\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\))+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))")
// FriendlyTime renders a timestamp in a friendly fashion: 03:04:05 PM if same day,
// otherwise Mon Jan 2, 2006
func FriendlyTime(t time.Time) template.HTML {
ty, tm, td := t.Date()
ny, nm, nd := time.Now().Date()
if (ty == ny) && (tm == nm) && (td == nd) {
return template.HTML(t.Format("03:04:05 PM"))
}
return template.HTML(t.Format("Mon Jan 2, 2006"))
}
// Reverse routing function (shared with templates)
func Reverse(name string, things ...interface{}) string {
// Convert the things to strings
strs := make([]string, len(things))
for i, th := range things {
strs[i] = fmt.Sprint(th)
}
// Grab the route
u, err := Router.Get(name).URL(strs...)
if err != nil {
log.Errorf("Failed to reverse route: %v", err)
return "/ROUTE-ERROR"
}
return u.Path
}
// TextToHTML takes plain text, escapes it and tries to pretty it up for
// HTML display
func TextToHTML(text string) template.HTML {
text = html.EscapeString(text)
text = urlRE.ReplaceAllStringFunc(text, WrapURL)
replacer := strings.NewReplacer("\r\n", "<br/>\n", "\r", "<br/>\n", "\n", "<br/>\n")
return template.HTML(replacer.Replace(text))
}
// WrapURL wraps a <a href> tag around the provided URL
func WrapURL(url string) string {
unescaped := strings.Replace(url, "&amp;", "&", -1)
return fmt.Sprintf("<a href=\"%s\" target=\"_blank\">%s</a>", unescaped, url)
}

View File

@@ -1,30 +0,0 @@
package httpd
import (
"html/template"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTextToHtml(t *testing.T) {
// Identity
assert.Equal(t, TextToHTML("html"), template.HTML("html"))
// Check it escapes
assert.Equal(t, TextToHTML("<html>"), template.HTML("&lt;html&gt;"))
// Check for linebreaks
assert.Equal(t, TextToHTML("line\nbreak"), template.HTML("line<br/>\nbreak"))
assert.Equal(t, TextToHTML("line\r\nbreak"), template.HTML("line<br/>\nbreak"))
assert.Equal(t, TextToHTML("line\rbreak"), template.HTML("line<br/>\nbreak"))
}
func TestURLDetection(t *testing.T) {
assert.Equal(t,
TextToHTML("http://google.com/"),
template.HTML("<a href=\"http://google.com/\" target=\"_blank\">http://google.com/</a>"))
assert.Equal(t,
TextToHTML("http://a.com/?q=a&n=v"),
template.HTML("<a href=\"http://a.com/?q=a&n=v\" target=\"_blank\">http://a.com/?q=a&amp;n=v</a>"))
}

View File

@@ -1,153 +0,0 @@
// Package httpd provides the plumbing for Inbucket's web GUI and RESTful API
package httpd
import (
"fmt"
"net"
"net/http"
"time"
"github.com/goods/httpbuf"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
"github.com/jhillyerd/inbucket/config"
"github.com/jhillyerd/inbucket/log"
"github.com/jhillyerd/inbucket/smtpd"
)
// Handler is a function type that handles an HTTP request in Inbucket
type Handler func(http.ResponseWriter, *http.Request, *Context) error
var (
// DataStore is where all the mailboxes and messages live
DataStore smtpd.DataStore
// Router is shared between httpd, webui and rest packages. It sends
// incoming requests to the correct handler function
Router = mux.NewRouter()
webConfig config.WebConfig
server *http.Server
listener net.Listener
sessionStore sessions.Store
globalShutdown chan bool
)
// Initialize sets up things for unit tests or the Start() method
func Initialize(cfg config.WebConfig, ds smtpd.DataStore, shutdownChan chan bool) {
webConfig = cfg
globalShutdown = shutdownChan
// NewContext() will use this DataStore for the web handlers
DataStore = ds
// Content Paths
log.Infof("HTTP templates mapped to %q", cfg.TemplateDir)
log.Infof("HTTP static content mapped to %q", cfg.PublicDir)
Router.PathPrefix("/public/").Handler(http.StripPrefix("/public/",
http.FileServer(http.Dir(cfg.PublicDir))))
http.Handle("/", Router)
// Session cookie setup
if cfg.CookieAuthKey == "" {
log.Infof("HTTP generating random cookie.auth.key")
sessionStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(64))
} else {
log.Tracef("HTTP using configured cookie.auth.key")
sessionStore = sessions.NewCookieStore([]byte(cfg.CookieAuthKey))
}
}
// Start begins listening for HTTP requests
func Start() {
addr := fmt.Sprintf("%v:%v", webConfig.IP4address, webConfig.IP4port)
server = &http.Server{
Addr: addr,
Handler: nil,
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
}
// We don't use ListenAndServe because it lacks a way to close the listener
log.Infof("HTTP listening on TCP4 %v", addr)
var err error
listener, err = net.Listen("tcp", addr)
if err != nil {
log.Errorf("HTTP failed to start TCP4 listener: %v", err)
emergencyShutdown()
return
}
// Listener go routine
go serve()
// Wait for shutdown
select {
case _ = <-globalShutdown:
log.Tracef("HTTP server shutting down on request")
}
// Closing the listener will cause the serve() go routine to exit
if err := listener.Close(); err != nil {
log.Errorf("Failed to close HTTP listener: %v", err)
}
}
// serve begins serving HTTP requests
func serve() {
// server.Serve blocks until we close the listener
err := server.Serve(listener)
select {
case _ = <-globalShutdown:
// Nop
default:
log.Errorf("HTTP server failed: %v", err)
emergencyShutdown()
return
}
}
// ServeHTTP builds the context and passes onto the real handler
func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Create the context
ctx, err := NewContext(req)
if err != nil {
log.Errorf("HTTP failed to create context: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer ctx.Close()
// Run the handler, grab the error, and report it
buf := new(httpbuf.Buffer)
log.Tracef("HTTP[%v] %v %v %q", req.RemoteAddr, req.Proto, req.Method, req.RequestURI)
err = h(buf, req, ctx)
if err != nil {
log.Errorf("HTTP error handling %q: %v", req.RequestURI, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Save the session
if err = ctx.Session.Save(req, buf); err != nil {
log.Errorf("HTTP failed to save session: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Apply the buffered response to the writer
if _, err = buf.Apply(w); err != nil {
log.Errorf("HTTP failed to write response: %v", err)
}
}
func emergencyShutdown() {
// Shutdown Inbucket
select {
case _ = <-globalShutdown:
default:
close(globalShutdown)
}
}

View File

@@ -1,83 +0,0 @@
package httpd
import (
"html/template"
"net/http"
"path"
"path/filepath"
"strings"
"sync"
"github.com/jhillyerd/inbucket/log"
)
var cachedMutex sync.Mutex
var cachedTemplates = map[string]*template.Template{}
var cachedPartials = map[string]*template.Template{}
// RenderTemplate fetches the named template and renders it to the provided
// ResponseWriter.
func RenderTemplate(name string, w http.ResponseWriter, data interface{}) error {
t, err := ParseTemplate(name, false)
if err != nil {
log.Errorf("Error in template '%v': %v", name, err)
return err
}
w.Header().Set("Expires", "-1")
return t.Execute(w, data)
}
// RenderPartial fetches the named template and renders it to the provided
// ResponseWriter.
func RenderPartial(name string, w http.ResponseWriter, data interface{}) error {
t, err := ParseTemplate(name, true)
if err != nil {
log.Errorf("Error in template '%v': %v", name, err)
return err
}
w.Header().Set("Expires", "-1")
return t.Execute(w, data)
}
// ParseTemplate loads the requested template along with _base.html, caching
// the result (if configured to do so)
func ParseTemplate(name string, partial bool) (*template.Template, error) {
cachedMutex.Lock()
defer cachedMutex.Unlock()
if t, ok := cachedTemplates[name]; ok {
return t, nil
}
tempPath := strings.Replace(name, "/", string(filepath.Separator), -1)
tempFile := filepath.Join(webConfig.TemplateDir, tempPath)
log.Tracef("Parsing template %v", tempFile)
var err error
var t *template.Template
if partial {
// Need to get basename of file to make it root template w/ funcs
base := path.Base(name)
t = template.New(base).Funcs(TemplateFuncs)
t, err = t.ParseFiles(tempFile)
} else {
t = template.New("_base.html").Funcs(TemplateFuncs)
t, err = t.ParseFiles(filepath.Join(webConfig.TemplateDir, "_base.html"), tempFile)
}
if err != nil {
return nil, err
}
// Allows us to disable caching for theme development
if webConfig.TemplateCache {
if partial {
log.Tracef("Caching partial %v", name)
cachedTemplates[name] = t
} else {
log.Tracef("Caching template %v", name)
cachedTemplates[name] = t
}
}
return t, nil
}

View File

@@ -1 +0,0 @@
inbucket.exe etc\win-sample.conf

View File

@@ -1,173 +0,0 @@
// main is the inbucket daemon launcher
package main
import (
"expvar"
"flag"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/jhillyerd/inbucket/config"
"github.com/jhillyerd/inbucket/httpd"
"github.com/jhillyerd/inbucket/log"
"github.com/jhillyerd/inbucket/pop3d"
"github.com/jhillyerd/inbucket/rest"
"github.com/jhillyerd/inbucket/smtpd"
"github.com/jhillyerd/inbucket/webui"
)
var (
// VERSION contains the build version number, populated during linking by goxc
VERSION = "1.1.0-rc1"
// BUILDDATE contains the build date, populated during linking by goxc
BUILDDATE = "undefined"
// Command line flags
help = flag.Bool("help", false, "Displays this help")
pidfile = flag.String("pidfile", "none", "Write our PID into the specified file")
logfile = flag.String("logfile", "stderr", "Write out log into the specified file")
// startTime is used to calculate uptime of Inbucket
startTime = time.Now()
// shutdownChan - close it to tell Inbucket to shut down cleanly
shutdownChan = make(chan bool)
// Server instances
smtpServer *smtpd.Server
pop3Server *pop3d.Server
)
func main() {
config.Version = VERSION
config.BuildDate = BUILDDATE
flag.Parse()
if *help {
flag.Usage()
return
}
// Load & Parse config
if flag.NArg() != 1 {
flag.Usage()
os.Exit(1)
}
err := config.LoadConfig(flag.Arg(0))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse config: %v\n", err)
os.Exit(1)
}
// Setup signal handler
sigChan := make(chan os.Signal)
signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
// Initialize logging
level, _ := config.Config.String("logging", "level")
log.SetLogLevel(level)
if err := log.Initialize(*logfile); err != nil {
fmt.Fprintf(os.Stderr, "%v", err)
os.Exit(1)
}
defer log.Close()
log.Infof("Inbucket %v (%v) starting...", config.Version, config.BuildDate)
// Write pidfile if requested
if *pidfile != "none" {
pidf, err := os.Create(*pidfile)
if err != nil {
log.Errorf("Failed to create %q: %v", *pidfile, err)
os.Exit(1)
}
fmt.Fprintf(pidf, "%v\n", os.Getpid())
if err := pidf.Close(); err != nil {
log.Errorf("Failed to close PID file %q: %v", *pidfile, err)
}
}
// Grab our datastore
ds := smtpd.DefaultFileDataStore()
// Start HTTP server
httpd.Initialize(config.GetWebConfig(), ds, shutdownChan)
webui.SetupRoutes(httpd.Router)
rest.SetupRoutes(httpd.Router)
go httpd.Start()
// Start POP3 server
// TODO pass datastore
pop3Server = pop3d.New(shutdownChan)
go pop3Server.Start()
// Startup SMTP server
smtpServer = smtpd.NewServer(config.GetSMTPConfig(), ds, shutdownChan)
go smtpServer.Start()
// Loop forever waiting for signals or shutdown channel
signalLoop:
for {
select {
case sig := <-sigChan:
switch sig {
case syscall.SIGHUP:
log.Infof("Recieved SIGHUP, cycling logfile")
log.Rotate()
case syscall.SIGINT:
// Shutdown requested
log.Infof("Received SIGINT, shutting down")
close(shutdownChan)
case syscall.SIGTERM:
// Shutdown requested
log.Infof("Received SIGTERM, shutting down")
close(shutdownChan)
}
case _ = <-shutdownChan:
break signalLoop
}
}
// Wait for active connections to finish
go timedExit()
smtpServer.Drain()
pop3Server.Drain()
removePIDFile()
}
// removePIDFile removes the PID file if created
func removePIDFile() {
if *pidfile != "none" {
if err := os.Remove(*pidfile); err != nil {
log.Errorf("Failed to remove %q: %v", *pidfile, err)
}
}
}
// timedExit is called as a goroutine during shutdown, it will force an exit
// after 15 seconds
func timedExit() {
time.Sleep(15 * time.Second)
log.Errorf("Clean shutdown took too long, forcing exit")
removePIDFile()
os.Exit(0)
}
func init() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "Usage of inbucket [options] <conf file>:")
flag.PrintDefaults()
}
expvar.Publish("uptime", expvar.Func(uptime))
}
// uptime() is published as an expvar
func uptime() interface{} {
return time.Since(startTime) / time.Second
}

View File

@@ -1,145 +0,0 @@
package log
import (
"fmt"
golog "log"
"os"
"strings"
)
// Level is used to indicate the severity of a log entry
type Level int
const (
// ERROR indicates a significant problem was encountered
ERROR Level = iota
// WARN indicates something that may be a problem
WARN
// INFO indicates a purely informational log entry
INFO
// TRACE entries are meant for development purposes only
TRACE
)
var (
// MaxLevel is the highest Level we will log (max TRACE, min ERROR)
MaxLevel = TRACE
// logfname is the name of the logfile
logfname string
// logf is the file we send log output to, will be nil for stderr or stdout
logf *os.File
)
// Initialize logging. If logfile is equal to "stderr" or "stdout", then
// we will log to that output stream. Otherwise the specificed file will
// opened for writing, and all log data will be placed in it.
func Initialize(logfile string) error {
if logfile != "stderr" {
// stderr is the go logging default
if logfile == "stdout" {
// set to stdout
golog.SetOutput(os.Stdout)
} else {
logfname = logfile
if err := openLogFile(); err != nil {
return err
}
// Platform specific
closeStdin()
}
}
return nil
}
// SetLogLevel sets MaxLevel based on the provided string
func SetLogLevel(level string) (ok bool) {
switch strings.ToUpper(level) {
case "ERROR":
MaxLevel = ERROR
case "WARN":
MaxLevel = WARN
case "INFO":
MaxLevel = INFO
case "TRACE":
MaxLevel = TRACE
default:
Errorf("Unknown log level requested: " + level)
return false
}
return true
}
// Errorf logs a message to the 'standard' Logger (always), accepts format strings
func Errorf(msg string, args ...interface{}) {
msg = "[ERROR] " + msg
golog.Printf(msg, args...)
}
// Warnf logs a message to the 'standard' Logger if MaxLevel is >= WARN, accepts format strings
func Warnf(msg string, args ...interface{}) {
if MaxLevel >= WARN {
msg = "[WARN ] " + msg
golog.Printf(msg, args...)
}
}
// Infof logs a message to the 'standard' Logger if MaxLevel is >= INFO, accepts format strings
func Infof(msg string, args ...interface{}) {
if MaxLevel >= INFO {
msg = "[INFO ] " + msg
golog.Printf(msg, args...)
}
}
// Tracef logs a message to the 'standard' Logger if MaxLevel is >= TRACE, accepts format strings
func Tracef(msg string, args ...interface{}) {
if MaxLevel >= TRACE {
msg = "[TRACE] " + msg
golog.Printf(msg, args...)
}
}
// Rotate closes the current log file, then reopens it. This gives an external
// log rotation system the opportunity to move the existing log file out of the
// way and have Inbucket create a new one.
func Rotate() {
// Rotate logs if configured
if logf != nil {
closeLogFile()
// There is nothing we can do if the log open fails
_ = openLogFile()
} else {
Infof("Ignoring SIGHUP, logfile not configured")
}
}
// Close the log file if we have one open
func Close() {
if logf != nil {
closeLogFile()
}
}
// openLogFile creates or appends to the logfile passed on commandline
func openLogFile() error {
// use specified log file
var err error
logf, err = os.OpenFile(logfname, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
return fmt.Errorf("Failed to create %v: %v\n", logfname, err)
}
golog.SetOutput(logf)
Tracef("Opened new logfile")
// Platform specific
reassignStdout()
return nil
}
// closeLogFile closes the current logfile
func closeLogFile() {
Tracef("Closing logfile")
// We are never in a situation where we can do anything about failing to close
_ = logf.Close()
}

View File

@@ -1,31 +0,0 @@
// +build !windows
package log
import (
"golang.org/x/sys/unix"
"os"
)
// closeStdin will close stdin on Unix platforms - this is standard practice
// for daemons
func closeStdin() {
if err := os.Stdin.Close(); err != nil {
// Not a fatal error
Errorf("Failed to close os.Stdin during log setup")
}
}
// reassignStdout points stdout/stderr to our logfile on systems that support
// the Dup2 syscall per https://github.com/golang/go/issues/325
func reassignStdout() {
Tracef("Unix reassignStdout()")
if err := unix.Dup2(int(logf.Fd()), 1); err != nil {
// Not considered fatal
Errorf("Failed to re-assign stdout to logfile: %v", err)
}
if err := unix.Dup2(int(logf.Fd()), 2); err != nil {
// Not considered fatal
Errorf("Failed to re-assign stderr to logfile: %v", err)
}
}

View File

@@ -1,37 +0,0 @@
// +build windows
package log
import (
"os"
)
var stdOutsClosed = false
// closeStdin does nothing on Windows, it would always fail
func closeStdin() {
// Nop
}
// reassignStdout points stdout/stderr to our logfile on systems that do not
// support the Dup2 syscall
func reassignStdout() {
Tracef("Windows reassignStdout()")
if !stdOutsClosed {
// Close std* streams to prevent accidental output, they will be redirected to
// our logfile below
// Warning: this will hide panic() output, sorry Windows users
if err := os.Stderr.Close(); err != nil {
// Not considered fatal
Errorf("Failed to close os.Stderr during log setup")
}
if err := os.Stdin.Close(); err != nil {
// Not considered fatal
Errorf("Failed to close os.Stdin during log setup")
}
os.Stdout = logf
os.Stderr = logf
stdOutsClosed = true
}
}

148
pkg/config/config.go Normal file
View File

@@ -0,0 +1,148 @@
package config
import (
"fmt"
"log"
"os"
"strings"
"text/tabwriter"
"time"
"github.com/inbucket/inbucket/v3/pkg/stringutil"
"github.com/kelseyhightower/envconfig"
)
const (
prefix = "inbucket"
tableFormat = `Inbucket is configured via the environment. The following environment variables
can be used:
KEY DEFAULT DESCRIPTION
{{range .}}{{usage_key .}} {{usage_default .}} {{usage_description .}}
{{end}}`
)
var (
// Version of this build, set by main
Version = ""
// BuildDate for this build, set by main
BuildDate = ""
)
// mbNaming represents a mailbox naming strategy.
type mbNaming int
// Mailbox naming strategies.
const (
UnknownNaming mbNaming = iota
LocalNaming
FullNaming
DomainNaming
)
// Decode a naming strategy from string.
func (n *mbNaming) Decode(v string) error {
switch strings.ToLower(v) {
case "local":
*n = LocalNaming
case "full":
*n = FullNaming
case "domain":
*n = DomainNaming
default:
return fmt.Errorf("unknown MailboxNaming strategy: %q", v)
}
return nil
}
// Root contains global configuration, and structs with for specific sub-systems.
type Root struct {
LogLevel string `required:"true" default:"info" desc:"debug, info, warn, or error"`
Lua Lua
MailboxNaming mbNaming `required:"true" default:"local" desc:"Use local, full, or domain addressing"`
SMTP SMTP
POP3 POP3
Web Web
Storage Storage
}
// Lua contains the Lua extension host configuration.
type Lua struct {
Path string `required:"false" default:"inbucket.lua" desc:"Lua script path"`
}
// SMTP contains the SMTP server configuration.
type SMTP struct {
Addr string `required:"true" default:"0.0.0.0:2500" desc:"SMTP server IP4 host:port"`
Domain string `required:"true" default:"inbucket" desc:"HELO domain"`
MaxRecipients int `required:"true" default:"200" desc:"Maximum RCPT TO per message"`
MaxMessageBytes int `required:"true" default:"10240000" desc:"Maximum message size"`
DefaultAccept bool `required:"true" default:"true" desc:"Accept all mail by default?"`
AcceptDomains []string `desc:"Domains to accept mail for"`
RejectDomains []string `desc:"Domains to reject mail for"`
DefaultStore bool `required:"true" default:"true" desc:"Store all mail by default?"`
StoreDomains []string `desc:"Domains to store mail for"`
DiscardDomains []string `desc:"Domains to discard mail for"`
RejectOriginDomains []string `desc:"Domains to reject mail from"`
Timeout time.Duration `required:"true" default:"300s" desc:"Idle network timeout"`
TLSEnabled bool `default:"false" desc:"Enable STARTTLS option"`
TLSPrivKey string `default:"cert.key" desc:"X509 Private Key file for TLS Support"`
TLSCert string `default:"cert.crt" desc:"X509 Public Certificate file for TLS Support"`
Debug bool `ignored:"true"`
ForceTLS bool `default:"false" desc:"Listen for connections with TLS."`
}
// POP3 contains the POP3 server configuration.
type POP3 struct {
Addr string `required:"true" default:"0.0.0.0:1100" desc:"POP3 server IP4 host:port"`
Domain string `required:"true" default:"inbucket" desc:"HELLO domain"`
Timeout time.Duration `required:"true" default:"600s" desc:"Idle network timeout"`
Debug bool `ignored:"true"`
TLSEnabled bool `default:"false" desc:"Enable TLS"`
TLSPrivKey string `default:"cert.key" desc:"X509 Private Key file for TLS Support"`
TLSCert string `default:"cert.crt" desc:"X509 Public Certificate file for TLS Support"`
ForceTLS bool `default:"false" desc:"If true, TLS is always on. If false, enable STLS"`
}
// Web contains the HTTP server configuration.
type Web struct {
Addr string `required:"true" default:"0.0.0.0:9000" desc:"Web server IP4 host:port"`
BasePath string `default:"" desc:"Base path prefix for UI and API URLs"`
UIDir string `required:"true" default:"ui/dist" desc:"User interface dir"`
GreetingFile string `required:"true" default:"ui/greeting.html" desc:"Home page greeting HTML"`
MonitorVisible bool `required:"true" default:"true" desc:"Show monitor tab in UI?"`
MonitorHistory int `required:"true" default:"30" desc:"Monitor remembered messages"`
PProf bool `required:"true" default:"false" desc:"Expose profiling tools on /debug/pprof"`
}
// Storage contains the mail store configuration.
type Storage struct {
Type string `required:"true" default:"memory" desc:"Storage impl: file or memory"`
Params map[string]string `desc:"Storage impl parameters, see docs."`
RetentionPeriod time.Duration `required:"true" default:"24h" desc:"Duration to retain messages"`
RetentionSleep time.Duration `required:"true" default:"50ms" desc:"Duration to sleep between mailboxes"`
MailboxMsgCap int `required:"true" default:"500" desc:"Maximum messages per mailbox"`
}
// Process loads and parses configuration from the environment.
func Process() (*Root, error) {
c := &Root{}
err := envconfig.Process(prefix, c)
c.LogLevel = strings.ToLower(c.LogLevel)
stringutil.SliceToLower(c.SMTP.AcceptDomains)
stringutil.SliceToLower(c.SMTP.RejectDomains)
stringutil.SliceToLower(c.SMTP.StoreDomains)
stringutil.SliceToLower(c.SMTP.DiscardDomains)
stringutil.SliceToLower(c.SMTP.RejectOriginDomains)
return c, err
}
// Usage prints out the envconfig usage to Stderr.
func Usage() {
tabs := tabwriter.NewWriter(os.Stderr, 1, 0, 4, ' ', 0)
if err := envconfig.Usagef(prefix, &Root{}, tabs, tableFormat); err != nil {
log.Fatalf("Unable to parse env config: %v", err)
}
tabs.Flush()
}

View File

@@ -0,0 +1,89 @@
package extension
import (
"errors"
"sync"
"time"
)
// AsyncEventBroker maintains a list of listeners interested in a specific type
// of event. Events are sent in parallel to all listeners, and no result is
// returned.
type AsyncEventBroker[E any] struct {
sync.RWMutex
listenerNames []string // Ordered listener names.
listenerFuncs []func(E) // Ordered listener functions.
}
// Emit sends the provided event to each registered listener in parallel.
func (eb *AsyncEventBroker[E]) Emit(event *E) {
eb.RLock()
defer eb.RUnlock()
for _, l := range eb.listenerFuncs {
// Events are copied to minimize the risk of mutation.
go l(*event)
}
}
// AddListener registers the named listener, replacing one with a duplicate
// name if present. Listeners should be added in order of priority, most
// significant first.
func (eb *AsyncEventBroker[E]) AddListener(name string, listener func(E)) {
eb.Lock()
defer eb.Unlock()
eb.lockedRemoveListener(name)
eb.listenerNames = append(eb.listenerNames, name)
eb.listenerFuncs = append(eb.listenerFuncs, listener)
}
// RemoveListener unregisters the named listener.
func (eb *AsyncEventBroker[E]) RemoveListener(name string) {
eb.Lock()
defer eb.Unlock()
eb.lockedRemoveListener(name)
}
func (eb *AsyncEventBroker[E]) lockedRemoveListener(name string) {
for i, entry := range eb.listenerNames {
if entry == name {
eb.listenerNames = append(eb.listenerNames[:i], eb.listenerNames[i+1:]...)
eb.listenerFuncs = append(eb.listenerFuncs[:i], eb.listenerFuncs[i+1:]...)
break
}
}
}
// AsyncTestListener returns a func that will wait for an event and return it, or timeout
// with an error.
func (eb *AsyncEventBroker[E]) AsyncTestListener(name string, capacity int) func() (*E, error) {
// Send event down channel.
events := make(chan E, capacity)
eb.AddListener(name,
func(msg E) {
events <- msg
})
count := 0
return func() (*E, error) {
count++
defer func() {
if count >= capacity {
eb.RemoveListener(name)
close(events)
}
}()
select {
case event := <-events:
return &event, nil
case <-time.After(time.Second * 2):
return nil, errors.New("timeout waiting for event")
}
}
}

View File

@@ -0,0 +1,101 @@
package extension_test
import (
"testing"
"time"
"github.com/inbucket/inbucket/v3/pkg/extension"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Simple smoke test without using AsyncTestListener.
func TestAsyncBrokerEmitCallsOneListener(t *testing.T) {
broker := &extension.AsyncEventBroker[string]{}
// Setup listener.
events := make(chan string, 1)
listener := func(s string) {
events <- s
}
broker.AddListener("x", listener)
want := "bacon"
broker.Emit(&want)
var got string
select {
case event := <-events:
got = event
case <-time.After(time.Second * 2):
t.Fatal("Timeout waiting for event")
}
if got != want {
t.Errorf("Emit got %q, want %q", got, want)
}
}
func TestAsyncBrokerEmitCallsMultipleListeners(t *testing.T) {
broker := &extension.AsyncEventBroker[string]{}
// Setup listeners.
first := broker.AsyncTestListener("first", 1)
second := broker.AsyncTestListener("second", 1)
want := "hi"
broker.Emit(&want)
firstGot, err := first()
require.NoError(t, err)
assert.Equal(t, want, *firstGot)
secondGot, err := second()
require.NoError(t, err)
assert.Equal(t, want, *secondGot)
}
func TestAsyncBrokerAddingDuplicateNameReplacesPrevious(t *testing.T) {
broker := &extension.AsyncEventBroker[string]{}
// Setup listeners.
first := broker.AsyncTestListener("dup", 1)
second := broker.AsyncTestListener("dup", 1)
want := "hi"
broker.Emit(&want)
firstGot, err := first()
require.Error(t, err)
assert.Nil(t, firstGot)
secondGot, err := second()
require.NoError(t, err)
assert.Equal(t, want, *secondGot)
}
func TestAsyncBrokerRemovingListenerSuccessful(t *testing.T) {
broker := &extension.AsyncEventBroker[string]{}
// Setup listeners.
first := broker.AsyncTestListener("1", 1)
second := broker.AsyncTestListener("2", 1)
broker.RemoveListener("1")
want := "hi"
broker.Emit(&want)
firstGot, err := first()
require.Error(t, err)
assert.Nil(t, firstGot)
secondGot, err := second()
require.NoError(t, err)
assert.Equal(t, want, *secondGot)
}
func TestAsyncBrokerRemovingMissingListener(t *testing.T) {
broker := &extension.AsyncEventBroker[string]{}
broker.RemoveListener("doesn't crash")
}

59
pkg/extension/broker.go Normal file
View File

@@ -0,0 +1,59 @@
package extension
import (
"sync"
)
// EventBroker maintains a list of listeners interested in a specific type
// of event.
type EventBroker[E any, R interface{}] struct {
sync.RWMutex
listenerNames []string // Ordered listener names.
listenerFuncs []func(E) *R // Ordered listener functions.
}
// Emit sends the provided event to each registered listener in order, until
// one returns a non-nil result. That result will be returned to the caller.
func (eb *EventBroker[E, R]) Emit(event *E) *R {
eb.RLock()
defer eb.RUnlock()
for _, l := range eb.listenerFuncs {
// Events are copied to minimize the risk of mutation.
if result := l(*event); result != nil {
return result
}
}
return nil
}
// AddListener registers the named listener, replacing one with a duplicate
// name if present. Listeners should be added in order of priority, most
// significant first.
func (eb *EventBroker[E, R]) AddListener(name string, listener func(E) *R) {
eb.Lock()
defer eb.Unlock()
eb.lockedRemoveListener(name)
eb.listenerNames = append(eb.listenerNames, name)
eb.listenerFuncs = append(eb.listenerFuncs, listener)
}
// RemoveListener unregisters the named listener.
func (eb *EventBroker[E, R]) RemoveListener(name string) {
eb.Lock()
defer eb.Unlock()
eb.lockedRemoveListener(name)
}
func (eb *EventBroker[E, R]) lockedRemoveListener(name string) {
for i, entry := range eb.listenerNames {
if entry == name {
eb.listenerNames = append(eb.listenerNames[:i], eb.listenerNames[i+1:]...)
eb.listenerFuncs = append(eb.listenerFuncs[:i], eb.listenerFuncs[i+1:]...)
break
}
}
}

View File

@@ -0,0 +1,134 @@
package extension_test
import (
"testing"
"github.com/inbucket/inbucket/v3/pkg/extension"
)
func TestBrokerEmitCallsOneListener(t *testing.T) {
broker := &extension.EventBroker[string, bool]{}
// Setup listener.
var got string
listener := func(s string) *bool {
got = s
return nil
}
broker.AddListener("x", listener)
want := "bacon"
broker.Emit(&want)
if got != want {
t.Errorf("Emit got %q, want %q", got, want)
}
}
func TestBrokerEmitCallsMultipleListeners(t *testing.T) {
broker := &extension.EventBroker[string, bool]{}
// Setup listeners.
var firstGot, secondGot string
first := func(s string) *bool {
firstGot = s
return nil
}
second := func(s string) *bool {
secondGot = s
return nil
}
broker.AddListener("1", first)
broker.AddListener("2", second)
want := "hi"
broker.Emit(&want)
if firstGot != want {
t.Errorf("first got %q, want %q", firstGot, want)
}
if secondGot != want {
t.Errorf("second got %q, want %q", secondGot, want)
}
}
func TestBrokerEmitCapturesFirstResult(t *testing.T) {
broker := &extension.EventBroker[struct{}, string]{}
// Setup listeners.
makeListener := func(result *string) func(struct{}) *string {
return func(s struct{}) *string { return result }
}
first := "first"
second := "second"
broker.AddListener("0", makeListener(nil))
broker.AddListener("1", makeListener(&first))
broker.AddListener("2", makeListener(&second))
want := first
got := broker.Emit(&struct{}{})
if got == nil {
t.Errorf("Emit got nil, want %q", want)
} else if *got != want {
t.Errorf("Emit got %q, want %q", *got, want)
}
}
func TestBrokerAddingDuplicateNameReplacesPrevious(t *testing.T) {
broker := &extension.EventBroker[string, bool]{}
// Setup listeners.
var firstGot, secondGot string
first := func(s string) *bool {
firstGot = s
return nil
}
second := func(s string) *bool {
secondGot = s
return nil
}
broker.AddListener("dup", first)
broker.AddListener("dup", second)
want := "hi"
broker.Emit(&want)
if firstGot != "" {
t.Errorf("first got %q, want empty string", firstGot)
}
if secondGot != want {
t.Errorf("second got %q, want %q", secondGot, want)
}
}
func TestBrokerRemovingListenerSuccessful(t *testing.T) {
broker := &extension.EventBroker[string, bool]{}
// Setup listeners.
var firstGot, secondGot string
first := func(s string) *bool {
firstGot = s
return nil
}
second := func(s string) *bool {
secondGot = s
return nil
}
broker.AddListener("1", first)
broker.AddListener("2", second)
broker.RemoveListener("1")
want := "hi"
broker.Emit(&want)
if firstGot != "" {
t.Errorf("first got %q, want empty string", firstGot)
}
if secondGot != want {
t.Errorf("second got %q, want %q", secondGot, want)
}
}
func TestBrokerRemovingMissingListener(t *testing.T) {
broker := &extension.EventBroker[string, bool]{}
broker.RemoveListener("doesn't crash")
}

View File

@@ -0,0 +1,56 @@
package event
import (
"net/mail"
"time"
)
const (
// ActionDefer defers decision to built-in Inbucket logic.
ActionDefer = iota
// ActionAllow explicitly allows this event.
ActionAllow
// ActionDeny explicitly deny this event, typically with specified SMTP error.
ActionDeny
)
// AddressParts contains the local and domain parts of an email address.
type AddressParts struct {
Local string
Domain string
}
// InboundMessage contains the basic header and mailbox data for a message being received.
type InboundMessage struct {
Mailboxes []string
From *mail.Address
To []*mail.Address
Subject string
Size int64
}
// MessageMetadata contains the basic header data for a message event.
type MessageMetadata struct {
Mailbox string
ID string
From *mail.Address
To []*mail.Address
Date time.Time
Subject string
Size int64
Seen bool
}
// SMTPResponse describes the response to an SMTP policy check.
type SMTPResponse struct {
Action int // ActionDefer, ActionAllow, etc.
ErrorCode int // SMTP error code to respond with on deny.
ErrorMsg string // SMTP error message to respond with on deny.
}
// SMTPSession captures SMTP `MAIL FROM` & `RCPT TO` values prior to mail DATA being received.
type SMTPSession struct {
From *mail.Address
To []*mail.Address
RemoteAddr string
}

36
pkg/extension/host.go Normal file
View File

@@ -0,0 +1,36 @@
package extension
import (
"github.com/inbucket/inbucket/v3/pkg/extension/event"
)
// Host defines extension points for Inbucket.
type Host struct {
Events *Events
}
// Events defines all the event types supported by the extension host.
//
// Before-events provide an opportunity for extensions to alter how Inbucket responds to that type
// of event. These events are processed synchronously; expensive operations will reduce the
// perceived performance of Inbucket. The first listener in the list to respond with a non-nil
// value will determine the response, and the remaining listeners will not be called.
//
// After-events allow extensions to take an action after an event has completed. These events are
// processed asynchronously with respect to the rest of Inbuckets operation. However, an event
// listener will not be called until the one before it completes.
type Events struct {
AfterMessageDeleted AsyncEventBroker[event.MessageMetadata]
AfterMessageStored AsyncEventBroker[event.MessageMetadata]
BeforeMailFromAccepted EventBroker[event.SMTPSession, event.SMTPResponse]
BeforeMessageStored EventBroker[event.InboundMessage, event.InboundMessage]
BeforeRcptToAccepted EventBroker[event.SMTPSession, event.SMTPResponse]
}
// Void indicates the event emitter will ignore any value returned by listeners.
type Void struct{}
// NewHost creates a new extension host.
func NewHost() *Host {
return &Host{Events: &Events{}}
}

View File

@@ -0,0 +1,92 @@
package luahost
import (
"net/mail"
lua "github.com/yuin/gopher-lua"
)
const mailAddressName = "address"
func registerMailAddressType(ls *lua.LState) {
mt := ls.NewTypeMetatable(mailAddressName)
ls.SetGlobal(mailAddressName, mt)
// Static attributes.
ls.SetField(mt, "new", ls.NewFunction(newMailAddress))
// Methods.
ls.SetField(mt, "__index", ls.NewFunction(mailAddressIndex))
ls.SetField(mt, "__newindex", ls.NewFunction(mailAddressNewIndex))
}
func newMailAddress(ls *lua.LState) int {
val := &mail.Address{
Name: ls.CheckString(1),
Address: ls.CheckString(2),
}
ud := wrapMailAddress(ls, val)
ls.Push(ud)
return 1
}
func wrapMailAddress(ls *lua.LState, val *mail.Address) *lua.LUserData {
ud := ls.NewUserData()
ud.Value = val
ls.SetMetatable(ud, ls.GetTypeMetatable(mailAddressName))
return ud
}
func unwrapMailAddress(ud *lua.LUserData) (*mail.Address, bool) {
val, ok := ud.Value.(*mail.Address)
return val, ok
}
func checkMailAddress(ls *lua.LState, pos int) *mail.Address {
ud := ls.CheckUserData(pos)
if val, ok := ud.Value.(*mail.Address); ok {
return val
}
ls.ArgError(1, mailAddressName+" expected")
return nil
}
// Gets a field value from MailAddress user object. This emulates a Lua table,
// allowing `msg.subject` instead of a Lua object syntax of `msg:subject()`.
func mailAddressIndex(ls *lua.LState) int {
a := checkMailAddress(ls, 1)
field := ls.CheckString(2)
// Push the requested field's value onto the stack.
switch field {
case "name":
ls.Push(lua.LString(a.Name))
case "address":
ls.Push(lua.LString(a.Address))
default:
// Unknown field.
ls.Push(lua.LNil)
}
return 1
}
// Sets a field value on MailAddress user object. This emulates a Lua table,
// allowing `msg.subject = x` instead of a Lua object syntax of `msg:subject(x)`.
func mailAddressNewIndex(ls *lua.LState) int {
a := checkMailAddress(ls, 1)
index := ls.CheckString(2)
switch index {
case "name":
a.Name = ls.CheckString(3)
case "address":
a.Address = ls.CheckString(3)
default:
ls.RaiseError("invalid index %q", index)
}
return 0
}

View File

@@ -0,0 +1,56 @@
package luahost
import (
"net/mail"
"testing"
"github.com/inbucket/inbucket/v3/pkg/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMailAddressGetters(t *testing.T) {
want := &mail.Address{
Name: "Roberto I",
Address: "ri@example.com",
}
script := `
assert(addr, "addr should not be nil")
want = "Roberto I"
got = addr.name
assert(got == want, string.format("got name %q, want %q", got, want))
want = "ri@example.com"
got = addr.address
assert(got == want, string.format("got address %q, want %q", got, want))
`
ls, _ := test.NewLuaState()
registerMailAddressType(ls)
ls.SetGlobal("addr", wrapMailAddress(ls, want))
require.NoError(t, ls.DoString(script))
}
func TestMailAddressSetters(t *testing.T) {
want := &mail.Address{
Name: "Roberto I",
Address: "ri@example.com",
}
script := `
assert(addr, "addr should not be nil")
addr.name = "Roberto I"
addr.address = "ri@example.com"
`
ls, _ := test.NewLuaState()
registerMailAddressType(ls)
got := &mail.Address{}
ls.SetGlobal("addr", wrapMailAddress(ls, got))
require.NoError(t, ls.DoString(script))
assert.Equal(t, want, got)
}

View File

@@ -0,0 +1,135 @@
package luahost
import (
"fmt"
"net/mail"
"github.com/inbucket/inbucket/v3/pkg/extension/event"
lua "github.com/yuin/gopher-lua"
)
const inboundMessageName = "inbound_message"
func registerInboundMessageType(ls *lua.LState) {
mt := ls.NewTypeMetatable(inboundMessageName)
ls.SetGlobal(inboundMessageName, mt)
// Static attributes.
ls.SetField(mt, "new", ls.NewFunction(newInboundMessage))
// Methods.
ls.SetField(mt, "__index", ls.NewFunction(inboundMessageIndex))
ls.SetField(mt, "__newindex", ls.NewFunction(inboundMessageNewIndex))
}
func newInboundMessage(ls *lua.LState) int {
val := &event.InboundMessage{}
ud := wrapInboundMessage(ls, val)
ls.Push(ud)
return 1
}
func wrapInboundMessage(ls *lua.LState, val *event.InboundMessage) *lua.LUserData {
ud := ls.NewUserData()
ud.Value = val
ls.SetMetatable(ud, ls.GetTypeMetatable(inboundMessageName))
return ud
}
// Checks there is an InboundMessage at stack position `pos`, else throws Lua error.
func checkInboundMessage(ls *lua.LState, pos int) *event.InboundMessage {
ud := ls.CheckUserData(pos)
if v, ok := ud.Value.(*event.InboundMessage); ok {
return v
}
ls.ArgError(pos, inboundMessageName+" expected")
return nil
}
func unwrapInboundMessage(lv lua.LValue) (*event.InboundMessage, error) {
if ud, ok := lv.(*lua.LUserData); ok {
if v, ok := ud.Value.(*event.InboundMessage); ok {
return v, nil
}
}
return nil, fmt.Errorf("expected InboundMessage, got %q", lv.Type().String())
}
// Gets a field value from InboundMessage user object. This emulates a Lua table,
// allowing `msg.subject` instead of a Lua object syntax of `msg:subject()`.
func inboundMessageIndex(ls *lua.LState) int {
m := checkInboundMessage(ls, 1)
field := ls.CheckString(2)
// Push the requested field's value onto the stack.
switch field {
case "mailboxes":
lt := &lua.LTable{}
for _, v := range m.Mailboxes {
lt.Append(lua.LString(v))
}
ls.Push(lt)
case "from":
ls.Push(wrapMailAddress(ls, m.From))
case "to":
lt := &lua.LTable{}
for _, v := range m.To {
addr := v
lt.Append(wrapMailAddress(ls, addr))
}
ls.Push(lt)
case "subject":
ls.Push(lua.LString(m.Subject))
case "size":
ls.Push(lua.LNumber(m.Size))
default:
// Unknown field.
ls.Push(lua.LNil)
}
return 1
}
// Sets a field value on InboundMessage user object. This emulates a Lua table,
// allowing `msg.subject = x` instead of a Lua object syntax of `msg:subject(x)`.
func inboundMessageNewIndex(ls *lua.LState) int {
m := checkInboundMessage(ls, 1)
index := ls.CheckString(2)
switch index {
case "mailboxes":
lt := ls.CheckTable(3)
mailboxes := make([]string, 0, 16)
lt.ForEach(func(k, lv lua.LValue) {
if mb, ok := lv.(lua.LString); ok {
mailboxes = append(mailboxes, string(mb))
}
})
m.Mailboxes = mailboxes
case "from":
m.From = checkMailAddress(ls, 3)
case "to":
lt := ls.CheckTable(3)
to := make([]*mail.Address, 0, 16)
lt.ForEach(func(k, lv lua.LValue) {
if ud, ok := lv.(*lua.LUserData); ok {
// TODO should fail if wrong type + test.
if entry, ok := unwrapMailAddress(ud); ok {
to = append(to, entry)
}
}
})
m.To = to
case "subject":
m.Subject = ls.CheckString(3)
case "size":
ls.RaiseError("size is read-only")
default:
ls.RaiseError("invalid index %q", index)
}
return 0
}

View File

@@ -0,0 +1,75 @@
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/assert"
"github.com/stretchr/testify/require"
)
func TestInboundMessageGetters(t *testing.T) {
want := &event.InboundMessage{
Mailboxes: []string{"mb1", "mb2"},
From: &mail.Address{Name: "name1", Address: "addr1"},
To: []*mail.Address{
{Name: "name2", Address: "addr2"},
{Name: "name3", Address: "addr3"},
},
Subject: "subj1",
Size: 42,
}
script := `
assert(msg, "msg should not be nil")
assert_eq(msg.mailboxes, {"mb1", "mb2"})
assert_eq(msg.subject, "subj1")
assert_eq(msg.size, 42, "msg.size")
assert_eq(msg.from.name, "name1", "from.name")
assert_eq(msg.from.address, "addr1", "from.address")
assert_eq(#msg.to, 2, "#msg.to")
assert_eq(msg.to[1].name, "name2", "to[1].name")
assert_eq(msg.to[1].address, "addr2", "to[1].address")
assert_eq(msg.to[2].name, "name3", "to[2].name")
assert_eq(msg.to[2].address, "addr3", "to[2].address")
`
ls, _ := test.NewLuaState()
registerInboundMessageType(ls)
registerMailAddressType(ls)
ls.SetGlobal("msg", wrapInboundMessage(ls, want))
require.NoError(t, ls.DoString(script))
}
func TestInboundMessageSetters(t *testing.T) {
want := &event.InboundMessage{
Mailboxes: []string{"mb1", "mb2"},
From: &mail.Address{Name: "name1", Address: "addr1"},
To: []*mail.Address{
{Name: "name2", Address: "addr2"},
{Name: "name3", Address: "addr3"},
},
Subject: "subj1",
}
script := `
assert(msg, "msg should not be nil")
msg.mailboxes = {"mb1", "mb2"}
msg.subject = "subj1"
msg.from = address.new("name1", "addr1")
msg.to = { address.new("name2", "addr2"), address.new("name3", "addr3") }
`
got := &event.InboundMessage{}
ls, _ := test.NewLuaState()
registerInboundMessageType(ls)
registerMailAddressType(ls)
ls.SetGlobal("msg", wrapInboundMessage(ls, got))
require.NoError(t, ls.DoString(script))
assert.Equal(t, want, got)
}

View File

@@ -0,0 +1,228 @@
package luahost
import (
"errors"
"fmt"
lua "github.com/yuin/gopher-lua"
)
const (
inbucketName = "inbucket"
inbucketBeforeName = "inbucket_before"
inbucketAfterName = "inbucket_after"
)
// Inbucket is the primary Lua interface data structure.
type Inbucket struct {
After InbucketAfterFuncs
Before InbucketBeforeFuncs
}
// InbucketAfterFuncs holds references to Lua extension functions to be called async
// after Inbucket handles an event.
type InbucketAfterFuncs struct {
MessageDeleted *lua.LFunction
MessageStored *lua.LFunction
}
// InbucketBeforeFuncs holds references to Lua extension functions to be called
// before Inbucket handles an event.
type InbucketBeforeFuncs struct {
MailFromAccepted *lua.LFunction
MessageStored *lua.LFunction
RcptToAccepted *lua.LFunction
}
func registerInbucketTypes(ls *lua.LState) {
// inbucket type.
mt := ls.NewTypeMetatable(inbucketName)
ls.SetField(mt, "__index", ls.NewFunction(inbucketIndex))
// inbucket global var.
ud := wrapInbucket(ls, &Inbucket{})
ls.SetGlobal(inbucketName, ud)
// inbucket.after type.
mt = ls.NewTypeMetatable(inbucketAfterName)
ls.SetField(mt, "__index", ls.NewFunction(inbucketAfterIndex))
ls.SetField(mt, "__newindex", ls.NewFunction(inbucketAfterNewIndex))
// inbucket.before type.
mt = ls.NewTypeMetatable(inbucketBeforeName)
ls.SetField(mt, "__index", ls.NewFunction(inbucketBeforeIndex))
ls.SetField(mt, "__newindex", ls.NewFunction(inbucketBeforeNewIndex))
}
func wrapInbucket(ls *lua.LState, val *Inbucket) *lua.LUserData {
ud := ls.NewUserData()
ud.Value = val
ls.SetMetatable(ud, ls.GetTypeMetatable(inbucketName))
return ud
}
func wrapInbucketAfter(ls *lua.LState, val *InbucketAfterFuncs) *lua.LUserData {
ud := ls.NewUserData()
ud.Value = val
ls.SetMetatable(ud, ls.GetTypeMetatable(inbucketAfterName))
return ud
}
func wrapInbucketBefore(ls *lua.LState, val *InbucketBeforeFuncs) *lua.LUserData {
ud := ls.NewUserData()
ud.Value = val
ls.SetMetatable(ud, ls.GetTypeMetatable(inbucketBeforeName))
return ud
}
func getInbucket(ls *lua.LState) (*Inbucket, error) {
lv := ls.GetGlobal(inbucketName)
if lv == nil {
return nil, errors.New("inbucket object was nil")
}
ud, ok := lv.(*lua.LUserData)
if !ok {
return nil, fmt.Errorf("inbucket object was type %s instead of UserData", lv.Type())
}
val, ok := ud.Value.(*Inbucket)
if !ok {
return nil, fmt.Errorf("inbucket object (%v) could not be cast", ud.Value)
}
return val, nil
}
func checkInbucket(ls *lua.LState, pos int) *Inbucket {
ud := ls.CheckUserData(pos)
if val, ok := ud.Value.(*Inbucket); ok {
return val
}
ls.ArgError(1, inbucketName+" expected")
return nil
}
func checkInbucketAfter(ls *lua.LState, pos int) *InbucketAfterFuncs {
ud := ls.CheckUserData(pos)
if val, ok := ud.Value.(*InbucketAfterFuncs); ok {
return val
}
ls.ArgError(1, inbucketAfterName+" expected")
return nil
}
func checkInbucketBefore(ls *lua.LState, pos int) *InbucketBeforeFuncs {
ud := ls.CheckUserData(pos)
if val, ok := ud.Value.(*InbucketBeforeFuncs); ok {
return val
}
ls.ArgError(1, inbucketBeforeName+" expected")
return nil
}
// inbucket getter.
func inbucketIndex(ls *lua.LState) int {
ib := checkInbucket(ls, 1)
field := ls.CheckString(2)
// Push the requested field's value onto the stack.
switch field {
case "after":
ls.Push(wrapInbucketAfter(ls, &ib.After))
case "before":
ls.Push(wrapInbucketBefore(ls, &ib.Before))
default:
// Unknown field.
ls.Push(lua.LNil)
}
return 1
}
// inbucket.after getter.
func inbucketAfterIndex(ls *lua.LState) int {
after := checkInbucketAfter(ls, 1)
field := ls.CheckString(2)
// Push the requested field's value onto the stack.
switch field {
case "message_deleted":
ls.Push(funcOrNil(after.MessageDeleted))
case "message_stored":
ls.Push(funcOrNil(after.MessageStored))
default:
// Unknown field.
ls.Push(lua.LNil)
}
return 1
}
// inbucket.after setter.
func inbucketAfterNewIndex(ls *lua.LState) int {
m := checkInbucketAfter(ls, 1)
index := ls.CheckString(2)
switch index {
case "message_deleted":
m.MessageDeleted = ls.CheckFunction(3)
case "message_stored":
m.MessageStored = ls.CheckFunction(3)
default:
ls.RaiseError("invalid inbucket.after index %q", index)
}
return 0
}
// inbucket.before getter.
func inbucketBeforeIndex(ls *lua.LState) int {
before := checkInbucketBefore(ls, 1)
field := ls.CheckString(2)
// Push the requested field's value onto the stack.
switch field {
case "mail_from_accepted":
ls.Push(funcOrNil(before.MailFromAccepted))
case "message_stored":
ls.Push(funcOrNil(before.MessageStored))
case "rcpt_to_accepted":
ls.Push(funcOrNil(before.RcptToAccepted))
default:
// Unknown field.
ls.Push(lua.LNil)
}
return 1
}
// inbucket.before setter.
func inbucketBeforeNewIndex(ls *lua.LState) int {
m := checkInbucketBefore(ls, 1)
index := ls.CheckString(2)
switch index {
case "mail_from_accepted":
m.MailFromAccepted = ls.CheckFunction(3)
case "message_stored":
m.MessageStored = ls.CheckFunction(3)
case "rcpt_to_accepted":
m.RcptToAccepted = ls.CheckFunction(3)
default:
ls.RaiseError("invalid inbucket.before index %q", index)
}
return 0
}
func funcOrNil(f *lua.LFunction) lua.LValue {
if f == nil {
return lua.LNil
}
return f
}

View File

@@ -0,0 +1,102 @@
package luahost
import (
"testing"
"github.com/inbucket/inbucket/v3/pkg/test"
"github.com/stretchr/testify/require"
)
func TestInbucketAfterFuncs(t *testing.T) {
// This Script registers each function and calls it. No effort is made to use the arguments
// that Inbucket expects, this is only to validate the inbucket.after data structure getters
// and setters.
script := `
assert(inbucket, "inbucket should not be nil")
assert(inbucket.after, "inbucket.after should not be nil")
local fns = { "message_deleted", "message_stored" }
-- Verify functions start off nil.
for i, name in ipairs(fns) do
assert(inbucket.after[name] == nil, "after." .. name .. " should be nil")
end
-- Test function to track func calls made, ensures no crossed wires.
local calls = {}
function makeTestFunc(create_name)
return function(call_name)
calls[create_name] = call_name
end
end
-- Set after functions, verify not nil, and call them.
for i, name in ipairs(fns) do
inbucket.after[name] = makeTestFunc(name)
assert(inbucket.after[name], "after." .. name .. " should not be nil")
end
-- Call each function. Separate loop to verify final state in 'calls'.
for i, name in ipairs(fns) do
inbucket.after[name](name)
end
-- Verify functions were called.
for i, name in ipairs(fns) do
assert(calls[name], "after." .. name .. " should have been called")
assert(calls[name] == name,
string.format("after.%s was called with incorrect argument %s", name, calls[name]))
end
`
ls, _ := test.NewLuaState()
registerInbucketTypes(ls)
require.NoError(t, ls.DoString(script))
}
func TestInbucketBeforeFuncs(t *testing.T) {
// This Script registers each function and calls it. No effort is made to use the arguments
// that Inbucket expects, this is only to validate the inbucket.before data structure getters
// and setters.
script := `
assert(inbucket, "inbucket should not be nil")
assert(inbucket.before, "inbucket.before should not be nil")
local fns = { "mail_from_accepted", "message_stored", "rcpt_to_accepted" }
-- Verify functions start off nil.
for i, name in ipairs(fns) do
assert(inbucket.before[name] == nil, "before." .. name .. " should be nil")
end
-- Test function to track func calls made, ensures no crossed wires.
local calls = {}
function makeTestFunc(create_name)
return function(call_name)
calls[create_name] = call_name
end
end
-- Set before functions, verify not nil, and call them.
for i, name in ipairs(fns) do
inbucket.before[name] = makeTestFunc(name)
assert(inbucket.before[name], "before." .. name .. " should not be nil")
end
-- Call each function. Separate loop to verify final state in 'calls'.
for i, name in ipairs(fns) do
inbucket.before[name](name)
end
-- Verify functions were called.
for i, name in ipairs(fns) do
assert(calls[name], "before." .. name .. " should have been called")
assert(calls[name] == name,
string.format("before.%s was called with incorrect argument %s", name, calls[name]))
end
`
ls, _ := test.NewLuaState()
registerInbucketTypes(ls)
require.NoError(t, ls.DoString(script))
}

View File

@@ -0,0 +1,120 @@
package luahost
import (
"net/mail"
"time"
"github.com/inbucket/inbucket/v3/pkg/extension/event"
lua "github.com/yuin/gopher-lua"
)
const messageMetadataName = "message_metadata"
func registerMessageMetadataType(ls *lua.LState) {
mt := ls.NewTypeMetatable(messageMetadataName)
ls.SetGlobal(messageMetadataName, mt)
// Static attributes.
ls.SetField(mt, "new", ls.NewFunction(newMessageMetadata))
// Methods.
ls.SetField(mt, "__index", ls.NewFunction(messageMetadataIndex))
ls.SetField(mt, "__newindex", ls.NewFunction(messageMetadataNewIndex))
}
func newMessageMetadata(ls *lua.LState) int {
val := &event.MessageMetadata{}
ud := wrapMessageMetadata(ls, val)
ls.Push(ud)
return 1
}
func wrapMessageMetadata(ls *lua.LState, val *event.MessageMetadata) *lua.LUserData {
ud := ls.NewUserData()
ud.Value = val
ls.SetMetatable(ud, ls.GetTypeMetatable(messageMetadataName))
return ud
}
func checkMessageMetadata(ls *lua.LState, pos int) *event.MessageMetadata {
ud := ls.CheckUserData(pos)
if v, ok := ud.Value.(*event.MessageMetadata); ok {
return v
}
ls.ArgError(1, messageMetadataName+" expected")
return nil
}
// Gets a field value from MessageMetadata user object. This emulates a Lua table,
// allowing `msg.subject` instead of a Lua object syntax of `msg:subject()`.
func messageMetadataIndex(ls *lua.LState) int {
m := checkMessageMetadata(ls, 1)
field := ls.CheckString(2)
// Push the requested field's value onto the stack.
switch field {
case "mailbox":
ls.Push(lua.LString(m.Mailbox))
case "id":
ls.Push(lua.LString(m.ID))
case "from":
ls.Push(wrapMailAddress(ls, m.From))
case "to":
lt := &lua.LTable{}
for _, v := range m.To {
lt.Append(wrapMailAddress(ls, v))
}
ls.Push(lt)
case "date":
ls.Push(lua.LNumber(m.Date.Unix()))
case "subject":
ls.Push(lua.LString(m.Subject))
case "size":
ls.Push(lua.LNumber(m.Size))
default:
// Unknown field.
ls.Push(lua.LNil)
}
return 1
}
// Sets a field value on MessageMetadata user object. This emulates a Lua table,
// allowing `msg.subject = x` instead of a Lua object syntax of `msg:subject(x)`.
func messageMetadataNewIndex(ls *lua.LState) int {
m := checkMessageMetadata(ls, 1)
index := ls.CheckString(2)
switch index {
case "mailbox":
m.Mailbox = ls.CheckString(3)
case "id":
m.ID = ls.CheckString(3)
case "from":
m.From = checkMailAddress(ls, 3)
case "to":
lt := ls.CheckTable(3)
to := make([]*mail.Address, 0, 16)
lt.ForEach(func(k, lv lua.LValue) {
if ud, ok := lv.(*lua.LUserData); ok {
// TODO should fail if wrong type + test.
if entry, ok := unwrapMailAddress(ud); ok {
to = append(to, entry)
}
}
})
m.To = to
case "date":
m.Date = time.Unix(ls.CheckInt64(3), 0)
case "subject":
m.Subject = ls.CheckString(3)
case "size":
m.Size = ls.CheckInt64(3)
default:
ls.RaiseError("invalid index %q", index)
}
return 0
}

View File

@@ -0,0 +1,87 @@
package luahost
import (
"net/mail"
"testing"
"time"
"github.com/inbucket/inbucket/v3/pkg/extension/event"
"github.com/inbucket/inbucket/v3/pkg/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMessageMetadataGetters(t *testing.T) {
want := &event.MessageMetadata{
Mailbox: "mb1",
ID: "id1",
From: &mail.Address{Name: "name1", Address: "addr1"},
To: []*mail.Address{{Name: "name2", Address: "addr2"}},
Date: time.Date(2001, time.February, 3, 4, 5, 6, 0, time.UTC),
Subject: "subj1",
Size: 42,
}
script := `
assert(msg, "msg should not be nil")
assert_eq(msg.mailbox, "mb1")
assert_eq(msg.id, "id1")
assert_eq(msg.subject, "subj1")
assert_eq(msg.size, 42, "msg.size")
assert_eq(msg.from.name, "name1", "from.name")
assert_eq(msg.from.address, "addr1", "from.address")
assert_eq(table.getn(msg.to), 1)
assert_eq(msg.to[1].name, "name2", "to.name")
assert_eq(msg.to[1].address, "addr2", "to.address")
assert_eq(msg.date, 981173106, "msg.date")
`
ls, _ := test.NewLuaState()
registerMessageMetadataType(ls)
registerMailAddressType(ls)
ls.SetGlobal("msg", wrapMessageMetadata(ls, want))
require.NoError(t, ls.DoString(script))
}
func TestMessageMetadataSetters(t *testing.T) {
want := &event.MessageMetadata{
Mailbox: "mb1",
ID: "id1",
From: &mail.Address{Name: "name1", Address: "addr1"},
To: []*mail.Address{{Name: "name2", Address: "addr2"}},
Date: time.Date(2001, time.February, 3, 4, 5, 6, 0, time.UTC),
Subject: "subj1",
Size: 42,
}
script := `
assert(msg, "msg should not be nil")
msg.mailbox = "mb1"
msg.id = "id1"
msg.subject = "subj1"
msg.size = 42
msg.from = address.new("name1", "addr1")
msg.to = { address.new("name2", "addr2") }
msg.date = 981173106
`
got := &event.MessageMetadata{}
ls, _ := test.NewLuaState()
registerMessageMetadataType(ls)
registerMailAddressType(ls)
ls.SetGlobal("msg", wrapMessageMetadata(ls, got))
require.NoError(t, ls.DoString(script))
// Timezones will cause a naive comparison to fail.
assert.Equal(t, want.Date.Unix(), got.Date.Unix())
now := time.Now()
want.Date = now
got.Date = now
assert.Equal(t, want, got)
}

View File

@@ -0,0 +1,54 @@
package luahost
import (
"fmt"
"github.com/inbucket/inbucket/v3/pkg/extension/event"
lua "github.com/yuin/gopher-lua"
)
const smtpResponseName = "smtp"
func registerSMTPResponseType(ls *lua.LState) {
mt := ls.NewTypeMetatable(smtpResponseName)
ls.SetGlobal(smtpResponseName, mt)
// Static attributes.
ls.SetField(mt, "allow", ls.NewFunction(newSMTPResponse(event.ActionAllow)))
ls.SetField(mt, "defer", ls.NewFunction(newSMTPResponse(event.ActionDefer)))
ls.SetField(mt, "deny", ls.NewFunction(newSMTPResponse(event.ActionDeny)))
}
func newSMTPResponse(action int) func(*lua.LState) int {
return func(ls *lua.LState) int {
val := &event.SMTPResponse{Action: action}
if action == event.ActionDeny {
// Optionally accept error code and message.
val.ErrorCode = ls.OptInt(1, 550)
val.ErrorMsg = ls.OptString(2, "Mail denied by policy")
}
ud := wrapSMTPResponse(ls, val)
ls.Push(ud)
return 1
}
}
func wrapSMTPResponse(ls *lua.LState, val *event.SMTPResponse) *lua.LUserData {
ud := ls.NewUserData()
ud.Value = val
ls.SetMetatable(ud, ls.GetTypeMetatable(smtpResponseName))
return ud
}
func unwrapSMTPResponse(lv lua.LValue) (*event.SMTPResponse, error) {
if ud, ok := lv.(*lua.LUserData); ok {
if v, ok := ud.Value.(*event.SMTPResponse); ok {
return v, nil
}
}
return nil, fmt.Errorf("expected SMTPResponse, got %q", lv.Type().String())
}

View File

@@ -0,0 +1,40 @@
package luahost
import (
"testing"
"github.com/inbucket/inbucket/v3/pkg/extension/event"
"github.com/inbucket/inbucket/v3/pkg/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSMTPResponseConstructors(t *testing.T) {
check := func(script string, want event.SMTPResponse) {
t.Helper()
ls, _ := test.NewLuaState()
registerSMTPResponseType(ls)
require.NoError(t, ls.DoString(script))
got, err := unwrapSMTPResponse(ls.Get(-1))
require.NoError(t, err)
assert.Equal(t, &want, got)
}
check("return smtp.defer()", event.SMTPResponse{Action: event.ActionDefer})
check("return smtp.allow()", event.SMTPResponse{Action: event.ActionAllow})
// Verify deny() has default code & msg.
check("return smtp.deny()", event.SMTPResponse{
Action: event.ActionDeny,
ErrorCode: 550,
ErrorMsg: "Mail denied by policy",
})
// Verify defaults can be overridden.
check("return smtp.deny(123, 'bacon')", event.SMTPResponse{
Action: event.ActionDeny,
ErrorCode: 123,
ErrorMsg: "bacon",
})
}

View File

@@ -0,0 +1,72 @@
package luahost
import (
"github.com/inbucket/inbucket/v3/pkg/extension/event"
lua "github.com/yuin/gopher-lua"
)
const smtpSessionName = "smtp_session"
func registerSMTPSessionType(ls *lua.LState) {
mt := ls.NewTypeMetatable(smtpSessionName)
ls.SetGlobal(smtpSessionName, mt)
// Static attributes.
ls.SetField(mt, "new", ls.NewFunction(newSMTPSession))
// Methods.
ls.SetField(mt, "__index", ls.NewFunction(smtpSessionIndex))
}
func newSMTPSession(ls *lua.LState) int {
val := &event.SMTPSession{}
ud := wrapSMTPSession(ls, val)
ls.Push(ud)
return 1
}
func wrapSMTPSession(ls *lua.LState, val *event.SMTPSession) *lua.LUserData {
ud := ls.NewUserData()
ud.Value = val
ls.SetMetatable(ud, ls.GetTypeMetatable(smtpSessionName))
return ud
}
// Checks there is an SMTPSession at stack position `pos`, else throws Lua error.
func checkSMTPSession(ls *lua.LState, pos int) *event.SMTPSession {
ud := ls.CheckUserData(pos)
if v, ok := ud.Value.(*event.SMTPSession); ok {
return v
}
ls.ArgError(pos, smtpSessionName+" expected")
return nil
}
// Gets a field value from SMTPSession user object. This emulates a Lua table,
// allowing `msg.subject` instead of a Lua object syntax of `msg:subject()`.
func smtpSessionIndex(ls *lua.LState) int {
session := checkSMTPSession(ls, 1)
field := ls.CheckString(2)
// Push the requested field's value onto the stack.
switch field {
case "from":
ls.Push(wrapMailAddress(ls, session.From))
case "to":
lt := &lua.LTable{}
for _, v := range session.To {
addr := v
lt.Append(wrapMailAddress(ls, addr))
}
ls.Push(lt)
case "remote_addr":
ls.Push(lua.LString(session.RemoteAddr))
default:
// Unknown field.
ls.Push(lua.LNil)
}
return 1
}

View File

@@ -0,0 +1,41 @@
package luahost
import (
"net/mail"
"testing"
"github.com/inbucket/inbucket/v3/pkg/extension/event"
"github.com/inbucket/inbucket/v3/pkg/test"
"github.com/stretchr/testify/require"
)
func TestSMTPSessionGetters(t *testing.T) {
want := &event.SMTPSession{
From: &mail.Address{Name: "name1", Address: "addr1"},
To: []*mail.Address{
{Name: "name2", Address: "addr2"},
{Name: "name3", Address: "addr3"},
},
RemoteAddr: "1.2.3.4",
}
script := `
assert(session, "session should not be nil")
assert_eq(session.from.name, "name1", "from.name")
assert_eq(session.from.address, "addr1", "from.address")
assert_eq(#session.to, 2, "#session.to")
assert_eq(session.to[1].name, "name2", "to[1].name")
assert_eq(session.to[1].address, "addr2", "to[1].address")
assert_eq(session.to[2].name, "name3", "to[2].name")
assert_eq(session.to[2].address, "addr3", "to[2].address")
assert_eq(session.remote_addr, "1.2.3.4")
`
ls, _ := test.NewLuaState()
registerSMTPSessionType(ls)
registerMailAddressType(ls)
ls.SetGlobal("session", wrapSMTPSession(ls, want))
require.NoError(t, ls.DoString(script))
}

View File

@@ -0,0 +1,259 @@
package luahost
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"github.com/inbucket/inbucket/v3/pkg/config"
"github.com/inbucket/inbucket/v3/pkg/extension"
"github.com/inbucket/inbucket/v3/pkg/extension/event"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
lua "github.com/yuin/gopher-lua"
"github.com/yuin/gopher-lua/parse"
)
// ErrNoScript signals that the Lua script file was not present.
var ErrNoScript error = errors.New("no script file present")
// Host of Lua extensions.
type Host struct {
extHost *extension.Host
pool *statePool
logContext zerolog.Context
}
// New constructs a new Lua Host, pre-compiling the source.
func New(conf config.Lua, extHost *extension.Host) (*Host, error) {
scriptPath := conf.Path
if scriptPath == "" {
return nil, nil
}
logContext := log.With().Str("module", "lua")
logger := logContext.Str("phase", "startup").Str("path", scriptPath).Logger()
// Pre-load, parse, and compile script.
if fi, err := os.Stat(scriptPath); err != nil {
logger.Info().Msg("Lua script file not found (this is not an error)")
return nil, ErrNoScript
} else if fi.IsDir() {
return nil, fmt.Errorf("lua script %v is a directory", scriptPath)
}
logger.Info().Msg("Loading script")
file, err := os.Open(scriptPath)
if err != nil {
return nil, err
}
defer file.Close()
return NewFromReader(logContext.Logger(), extHost, bufio.NewReader(file), scriptPath)
}
// NewFromReader constructs a new Lua Host, loading Lua source from the provided reader.
// The provided path is used in logging and error messages.
func NewFromReader(logger zerolog.Logger, extHost *extension.Host, r io.Reader, path string) (*Host, error) {
startLogger := logger.With().Str("phase", "startup").Str("path", path).Logger()
// Pre-parse, and compile script.
chunk, err := parse.Parse(r, path)
if err != nil {
return nil, err
}
proto, err := lua.Compile(chunk, path)
if err != nil {
return nil, err
}
// Build the pool and confirm LState is retrievable.
pool := newStatePool(logger, proto)
h := &Host{extHost: extHost, pool: pool, logContext: logger.With()}
if ls, err := pool.getState(); err == nil {
h.wireFunctions(startLogger, ls)
// State creation works, put it back.
pool.putState(ls)
} else {
return nil, err
}
return h, nil
}
// CreateChannel creates a channel and places it into the named global variable
// in newly created LStates.
func (h *Host) CreateChannel(name string) chan lua.LValue {
return h.pool.createChannel(name)
}
// Detects global lua event listener functions and wires them up.
func (h *Host) wireFunctions(logger zerolog.Logger, ls *lua.LState) {
ib, err := getInbucket(ls)
if err != nil {
logger.Fatal().Err(err).Msg("Failed to get inbucket global")
}
events := h.extHost.Events
const listenerName string = "lua"
if ib.After.MessageDeleted != nil {
events.AfterMessageDeleted.AddListener(listenerName, h.handleAfterMessageDeleted)
}
if ib.After.MessageStored != nil {
events.AfterMessageStored.AddListener(listenerName, h.handleAfterMessageStored)
}
if ib.Before.MailFromAccepted != nil {
events.BeforeMailFromAccepted.AddListener(listenerName, h.handleBeforeMailFromAccepted)
}
if ib.Before.MessageStored != nil {
events.BeforeMessageStored.AddListener(listenerName, h.handleBeforeMessageStored)
}
if ib.Before.RcptToAccepted != nil {
events.BeforeRcptToAccepted.AddListener(listenerName, h.handleBeforeRcptToAccepted)
}
}
func (h *Host) handleAfterMessageDeleted(msg event.MessageMetadata) {
logger, ls, ib, ok := h.prepareInbucketFuncCall("after.message_deleted")
if !ok {
return
}
defer h.pool.putState(ls)
// Call lua function.
logger.Debug().Msgf("Calling Lua function with %+v", msg)
if err := ls.CallByParam(
lua.P{Fn: ib.After.MessageDeleted, NRet: 0, Protect: true},
wrapMessageMetadata(ls, &msg),
); err != nil {
logger.Error().Err(err).Msg("Failed to call Lua function")
}
}
func (h *Host) handleAfterMessageStored(msg event.MessageMetadata) {
logger, ls, ib, ok := h.prepareInbucketFuncCall("after.message_stored")
if !ok {
return
}
defer h.pool.putState(ls)
// Call lua function.
logger.Debug().Msgf("Calling Lua function with %+v", msg)
if err := ls.CallByParam(
lua.P{Fn: ib.After.MessageStored, NRet: 0, Protect: true},
wrapMessageMetadata(ls, &msg),
); err != nil {
logger.Error().Err(err).Msg("Failed to call Lua function")
}
}
func (h *Host) handleBeforeMailFromAccepted(session event.SMTPSession) *event.SMTPResponse {
logger, ls, ib, ok := h.prepareInbucketFuncCall("before.mail_from_accepted")
if !ok {
return nil
}
defer h.pool.putState(ls)
logger.Debug().Msgf("Calling Lua function with %+v", session)
if err := ls.CallByParam(
lua.P{Fn: ib.Before.MailFromAccepted, NRet: 1, Protect: true},
wrapSMTPSession(ls, &session),
); err != nil {
logger.Error().Err(err).Msg("Failed to call Lua function")
return nil
}
lval := ls.Get(-1)
ls.Pop(1)
logger.Debug().Msgf("Lua function returned %q (%v)", lval, lval.Type().String())
result, err := unwrapSMTPResponse(lval)
if err != nil {
logger.Error().Err(err).Msg("Bad response from Lua Function")
}
return result
}
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
}
lval := ls.Get(-1)
ls.Pop(1)
logger.Debug().Msgf("Lua function returned %q (%v)", lval, lval.Type().String())
result, err := unwrapSMTPResponse(lval)
if err != nil {
logger.Error().Err(err).Msg("Bad response from Lua Function")
}
return result
}
func (h *Host) handleBeforeMessageStored(msg event.InboundMessage) *event.InboundMessage {
logger, ls, ib, ok := h.prepareInbucketFuncCall("before.message_stored")
if !ok {
return nil
}
defer h.pool.putState(ls)
logger.Debug().Msgf("Calling Lua function with %+v", msg)
if err := ls.CallByParam(
lua.P{Fn: ib.Before.MessageStored, NRet: 1, Protect: true},
wrapInboundMessage(ls, &msg),
); err != nil {
logger.Error().Err(err).Msg("Failed to call Lua function")
return nil
}
lval := ls.Get(-1)
ls.Pop(1)
logger.Debug().Msgf("Lua function returned %q (%v)", lval, lval.Type().String())
if lua.LVIsFalse(lval) {
return nil
}
result, err := unwrapInboundMessage(lval)
if err != nil {
logger.Error().Err(err).Msg("Bad response from Lua Function")
}
return result
}
// Common preparation for calling Lua functions.
func (h *Host) prepareInbucketFuncCall(funcName string) (logger zerolog.Logger, ls *lua.LState, ib *Inbucket, ok bool) {
logger = h.logContext.Str("event", funcName).Logger()
ls, err := h.pool.getState()
if err != nil {
logger.Error().Err(err).Msg("Failed to get Lua state instance from pool")
return logger, nil, nil, false
}
ib, err = getInbucket(ls)
if err != nil {
logger.Error().Err(err).Msg("Failed to obtain Lua inbucket object")
return logger, nil, nil, false
}
return logger, ls, ib, true
}

View File

@@ -0,0 +1,303 @@
package luahost_test
import (
"net/mail"
"strings"
"testing"
"time"
"github.com/inbucket/inbucket/v3/pkg/extension"
"github.com/inbucket/inbucket/v3/pkg/extension/event"
"github.com/inbucket/inbucket/v3/pkg/extension/luahost"
"github.com/inbucket/inbucket/v3/pkg/test"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var consoleLogger = zerolog.New(zerolog.NewConsoleWriter())
func TestEmptyScript(t *testing.T) {
script := ""
extHost := extension.NewHost()
_, err := luahost.NewFromReader(consoleLogger, extHost, strings.NewReader(script), "test.lua")
require.NoError(t, err)
}
func TestLogger(t *testing.T) {
script := `
local logger = require("logger")
logger.info("_test log entry_", {})
`
extHost := extension.NewHost()
output := &strings.Builder{}
logger := zerolog.New(output)
_, err := luahost.NewFromReader(logger, extHost, strings.NewReader(script), "test.lua")
require.NoError(t, err)
assert.Contains(t, output.String(), "_test log entry_")
}
func TestAfterMessageDeleted(t *testing.T) {
// Register lua event listener, setup notify channel.
script := `
async = true
function inbucket.after.message_deleted(msg)
-- Full message bindings tested elsewhere.
assert_eq(msg.mailbox, "mb1")
assert_eq(msg.id, "id1")
notify:send(asserts_ok)
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, check channel response is true.
msg := &event.MessageMetadata{
Mailbox: "mb1",
ID: "id1",
From: &mail.Address{Name: "name1", Address: "addr1"},
To: []*mail.Address{{Name: "name2", Address: "addr2"}},
Date: time.Date(2001, time.February, 3, 4, 5, 6, 0, time.UTC),
Subject: "subj1",
Size: 42,
}
extHost.Events.AfterMessageDeleted.Emit(msg)
test.AssertNotified(t, notify)
}
func TestAfterMessageStored(t *testing.T) {
// Register lua event listener, setup notify channel.
script := `
async = true
function inbucket.after.message_stored(msg)
-- Full message bindings tested elsewhere.
assert_eq(msg.mailbox, "mb1")
assert_eq(msg.id, "id1")
notify:send(asserts_ok)
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, check channel response is true.
msg := &event.MessageMetadata{
Mailbox: "mb1",
ID: "id1",
From: &mail.Address{Name: "name1", Address: "addr1"},
To: []*mail.Address{{Name: "name2", Address: "addr2"}},
Date: time.Date(2001, time.February, 3, 4, 5, 6, 0, time.UTC),
Subject: "subj1",
Size: 42,
}
extHost.Events.AfterMessageStored.Emit(msg)
test.AssertNotified(t, notify)
}
func TestBeforeMailFromAccepted(t *testing.T) {
// Register lua event listener.
script := `
function inbucket.before.mail_from_accepted(session)
if session.from.address == "from@example.com" then
logger.info("allowing message", {})
return smtp.allow()
else
logger.info("denying message", {})
return smtp.deny()
end
end
`
extHost := extension.NewHost()
_, err := luahost.NewFromReader(
consoleLogger, extHost, strings.NewReader(test.LuaInit+script), "test.lua")
require.NoError(t, err)
{
// Send event to be accepted.
session := event.SMTPSession{
From: &mail.Address{Name: "", Address: "from@example.com"},
}
got := extHost.Events.BeforeMailFromAccepted.Emit(&session)
want := event.ActionAllow
require.NotNil(t, got, "Expected result from Emit()")
if got.Action != want {
t.Errorf("Got %v, wanted %v for addr %v", got.Action, want, session.From)
}
}
{
// Send event to be denied.
session := event.SMTPSession{
From: &mail.Address{Name: "", Address: "from@reject.com"},
}
got := extHost.Events.BeforeMailFromAccepted.Emit(&session)
want := event.ActionDeny
require.NotNil(t, got, "Expected result from Emit()")
if got.Action != want {
t.Errorf("Got %v, wanted %v for addr %v", got.Action, want, session.From)
}
}
}
func TestBeforeMessageStored(t *testing.T) {
// Event to send.
msg := event.InboundMessage{
Mailboxes: []string{"one", "two"},
From: &mail.Address{Name: "From Name", Address: "from@example.com"},
To: []*mail.Address{
{Name: "To1 Name", Address: "to1@example.com"},
{Name: "To2 Name", Address: "to2@example.com"},
},
Subject: "inbound subj",
Size: 42,
}
// Register lua event listener.
script := `
async = true
function inbucket.before.message_stored(msg)
-- Verify incoming values.
assert_eq(msg.mailboxes, {"one", "two"})
assert_eq(msg.from.name, "From Name")
assert_eq(msg.from.address, "from@example.com")
assert_eq(2, #msg.to, "#msg.to")
assert_eq(msg.to[1].name, "To1 Name")
assert_eq(msg.to[1].address, "to1@example.com")
assert_eq(msg.to[2].name, "To2 Name")
assert_eq(msg.to[2].address, "to2@example.com")
assert_eq(msg.subject, "inbound subj")
assert_eq(msg.size, 42, "msg.size")
notify:send(asserts_ok)
-- Generate response.
res = inbound_message.new()
res.mailboxes = {"resone", "restwo"}
res.from = address.new("Res From", "res@example.com")
res.to = {
address.new("To1 Res", "res1@example.com"),
address.new("To2 Res", "res2@example.com"),
}
res.subject = "res subj"
return res
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.NotNil(t, got, "Expected result from Emit()")
// Verify Lua assertions passed.
test.AssertNotified(t, notify)
// Verify response values.
want := &event.InboundMessage{
Mailboxes: []string{"resone", "restwo"},
From: &mail.Address{Name: "Res From", Address: "res@example.com"},
To: []*mail.Address{
{Name: "To1 Res", Address: "res1@example.com"},
{Name: "To2 Res", Address: "res2@example.com"},
},
Subject: "res subj",
Size: 0,
}
assert.Equal(t, want, got, "Response InboundMessage did not match")
}
func TestBeforeMessageStoredNilReturn(t *testing.T) {
// Event to send.
msg := event.InboundMessage{
Mailboxes: []string{"one", "two"},
From: &mail.Address{Name: "From Name", Address: "from@example.com"},
To: []*mail.Address{
{Name: "To1 Name", Address: "to1@example.com"},
{Name: "To2 Name", Address: "to2@example.com"},
},
Subject: "inbound subj",
Size: 42,
}
// Register lua event listener.
script := `
async = true
function inbucket.before.message_stored(msg)
assert(msg)
notify:send(asserts_ok)
-- Generate response.
return nil
end
`
extHost := extension.NewHost()
luaHost, err := luahost.NewFromReader(consoleLogger, extHost,
strings.NewReader(test.LuaInit+script), "test.lua")
require.NoError(t, err)
notify := luaHost.CreateChannel("notify")
// Send event to be accepted.
got := extHost.Events.BeforeMessageStored.Emit(&msg)
require.Nil(t, got, "Expected nil result from Emit()")
// Verify Lua assertions passed.
test.AssertNotified(t, notify)
}
func TestBeforeRcptToAccepted(t *testing.T) {
// Event to send.
session := event.SMTPSession{
From: &mail.Address{Name: "", Address: "from@example.com"},
To: []*mail.Address{
{Name: "", Address: "to1@example.com"},
{Name: "", Address: "to2@example.com"},
},
}
// Register lua event listener.
script := `
async = true
function inbucket.before.rcpt_to_accepted(msg)
-- Verify incoming values.
assert_eq(msg.from.address, "from@example.com")
assert_eq(2, #msg.to, "#msg.to")
assert_eq(msg.to[1].address, "to1@example.com")
assert_eq(msg.to[2].address, "to2@example.com")
notify:send(asserts_ok)
return smtp.allow()
end
`
extHost := extension.NewHost()
luaHost, err := luahost.NewFromReader(consoleLogger, extHost,
strings.NewReader(test.LuaInit+script), "test.lua")
require.NoError(t, err)
notify := luaHost.CreateChannel("notify")
// Send event to be accepted.
got := extHost.Events.BeforeRcptToAccepted.Emit(&session)
require.NotNil(t, got, "Expected result from Emit()")
// Verify Lua assertions passed.
test.AssertNotified(t, notify)
// Verify response values.
want := event.SMTPResponse{Action: event.ActionAllow}
assert.Equal(t, want, *got)
}

View File

@@ -0,0 +1,113 @@
package luahost
import (
"net/http"
"sync"
"github.com/cjoudrey/gluahttp"
"github.com/cosmotek/loguago"
json "github.com/inbucket/gopher-json"
"github.com/rs/zerolog"
lua "github.com/yuin/gopher-lua"
)
type statePool struct {
sync.Mutex
funcProto *lua.FunctionProto // Compiled lua.
states []*lua.LState // Pool of available LStates.
channels map[string]chan lua.LValue // Global interop channels.
logger zerolog.Logger // Logger exported to Lua scripts.
}
func newStatePool(logger zerolog.Logger, funcProto *lua.FunctionProto) *statePool {
return &statePool{
funcProto: funcProto,
channels: make(map[string]chan lua.LValue),
logger: logger,
}
}
// newState creates a new LState and configures it. Lock must be held.
func (lp *statePool) newState() (*lua.LState, error) {
ls := lua.NewState()
logger := loguago.NewLogger(lp.logger)
// Load supplemental native modules.
ls.PreloadModule("http", gluahttp.NewHttpModule(&http.Client{}).Loader)
ls.PreloadModule("json", json.Loader)
ls.PreloadModule("logger", logger.Loader)
// Setup channels.
for name, ch := range lp.channels {
ls.SetGlobal(name, lua.LChannel(ch))
}
// Register custom types.
registerInboundMessageType(ls)
registerInbucketTypes(ls)
registerMailAddressType(ls)
registerMessageMetadataType(ls)
registerSMTPResponseType(ls)
registerSMTPSessionType(ls)
// Run compiled script.
ls.Push(ls.NewFunctionFromProto(lp.funcProto))
if err := ls.PCall(0, lua.MultRet, nil); err != nil {
return nil, err
}
return ls, nil
}
// getState returns a free LState, or creates a new one.
func (lp *statePool) getState() (*lua.LState, error) {
lp.Lock()
defer lp.Unlock()
ln := len(lp.states)
if ln == 0 {
return lp.newState()
}
state := lp.states[ln-1]
lp.states = lp.states[0 : ln-1]
return state, nil
}
// putState returns the LState to the pool.
func (lp *statePool) putState(state *lua.LState) {
if state.IsClosed() {
return
}
// Clear stack.
state.Pop(state.GetTop())
lp.Lock()
defer lp.Unlock()
lp.states = append(lp.states, state)
}
// createChannel creates a new channel, which will become a global variable in
// newly created LStates. We also destroy any pooled states.
//
// Warning: There may still be checked out LStates that will not have the value
// set, which could be put back into the pool.
func (lp *statePool) createChannel(name string) chan lua.LValue {
lp.Lock()
defer lp.Unlock()
ch := make(chan lua.LValue, 10)
lp.channels[name] = ch
// Flush state pool.
for _, s := range lp.states {
s.Close()
}
lp.states = lp.states[:0]
return ch
}

View File

@@ -0,0 +1,102 @@
package luahost
import (
"strings"
"testing"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
lua "github.com/yuin/gopher-lua"
"github.com/yuin/gopher-lua/parse"
)
func makeEmptyPool() *statePool {
source := strings.NewReader("-- Empty source")
chunk, err := parse.Parse(source, "from string")
if err != nil {
panic(err)
}
proto, err := lua.Compile(chunk, "from string")
if err != nil {
panic(err)
}
return newStatePool(zerolog.Nop(), proto)
}
func TestPoolGetsDistinct(t *testing.T) {
pool := makeEmptyPool()
a, err := pool.getState()
require.NoError(t, err)
b, err := pool.getState()
require.NoError(t, err)
if a == b {
t.Error("Got pool a == b, expected distinct pools")
}
}
func TestPoolGrowsWithPuts(t *testing.T) {
pool := makeEmptyPool()
a, err := pool.getState()
require.NoError(t, err)
b, err := pool.getState()
require.NoError(t, err)
assert.Empty(t, pool.states, "Wanted pool to be empty")
pool.putState(a)
pool.putState(b)
want := 2
if got := len(pool.states); got != want {
t.Errorf("len pool.states got %v, want %v", got, want)
}
}
// Closed LStates should not be added to the pool.
func TestPoolPutDiscardsClosed(t *testing.T) {
pool := makeEmptyPool()
a, err := pool.getState()
require.NoError(t, err)
assert.Empty(t, pool.states, "Wanted pool to be empty")
a.Close()
pool.putState(a)
assert.Empty(t, pool.states, "Wanted pool to remain empty")
}
func TestPoolPutClearsStack(t *testing.T) {
pool := makeEmptyPool()
ls, err := pool.getState()
require.NoError(t, err)
assert.Empty(t, pool.states, "Wanted pool to be empty")
// Setup stack.
ls.Push(lua.LNumber(4))
ls.Push(lua.LString("bacon"))
require.Equal(t, 2, ls.GetTop(), "Want stack to have two items")
// Return and verify stack cleared.
pool.putState(ls)
assert.Len(t, pool.states, 1, "Wanted pool to have one item")
require.Equal(t, 0, ls.GetTop(), "Want stack to be empty")
}
func TestPoolSetsChannels(t *testing.T) {
pool := makeEmptyPool()
pool.createChannel("test_chan")
s, err := pool.getState()
require.NoError(t, err)
got := s.GetGlobal("test_chan")
assert.Equal(t, lua.LTChannel, got.Type(),
"Got global type %v, wanted LTChannel", got.Type().String())
}

219
pkg/message/manager.go Normal file
View File

@@ -0,0 +1,219 @@
package message
import (
"bytes"
"fmt"
"io"
"net/mail"
"strings"
"time"
"github.com/inbucket/inbucket/v3/pkg/extension"
"github.com/inbucket/inbucket/v3/pkg/extension/event"
"github.com/inbucket/inbucket/v3/pkg/policy"
"github.com/inbucket/inbucket/v3/pkg/storage"
"github.com/jhillyerd/enmime/v2"
"github.com/rs/zerolog/log"
)
// recvdTimeFmt to use in generated Received header.
const recvdTimeFmt = "Mon, 02 Jan 2006 15:04:05 -0700 (MST)"
// Manager is the interface controllers use to interact with messages.
type Manager interface {
Deliver(
from *policy.Origin,
recipients []*policy.Recipient,
recvdHeader string,
content []byte,
) error
GetMetadata(mailbox string) ([]*event.MessageMetadata, error)
GetMessage(mailbox, id string) (*Message, error)
MarkSeen(mailbox, id string) error
PurgeMessages(mailbox string) error
RemoveMessage(mailbox, id string) error
SourceReader(mailbox, id string) (io.ReadCloser, error)
MailboxForAddress(address string) (string, error)
}
// StoreManager is a message Manager backed by the storage.Store.
type StoreManager struct {
AddrPolicy *policy.Addressing
Store storage.Store
ExtHost *extension.Host
}
// Deliver submits a new message to the store.
func (s *StoreManager) Deliver(
from *policy.Origin,
recipients []*policy.Recipient,
recvdHeader string,
source []byte,
) error {
logger := log.With().Str("module", "message").Logger()
// Parse envelope headers.
header, err := enmime.DecodeHeaders(source)
if err != nil {
return err
}
fromAddrs, err := enmime.ParseAddressList(header.Get("From"))
if err != nil || len(fromAddrs) == 0 {
// Failed to parse From header, use SMTP MAIL FROM instead.
fromAddrs = make([]*mail.Address, 1)
fromAddrs[0] = &from.Address
}
toAddrs, err := enmime.ParseAddressList(header.Get("To"))
if err != nil {
// Failed to parse To header, use SMTP RCPT TO instead.
toAddrs = make([]*mail.Address, len(recipients))
for i, torecip := range recipients {
toAddrs[i] = &torecip.Address
}
}
subject := header.Get("Subject")
now := time.Now()
tstamp := now.UTC().Format(recvdTimeFmt)
// Process inbound message through extensions.
mailboxes := make([]string, 0, len(recipients))
for _, recip := range recipients {
mailboxes = append(mailboxes, recip.Mailbox)
}
// Construct InboundMessage event and process through extensions.
inbound := &event.InboundMessage{
Mailboxes: mailboxes,
From: fromAddrs[0],
To: toAddrs,
Subject: subject,
Size: int64(len(source)),
}
extResult := s.ExtHost.Events.BeforeMessageStored.Emit(inbound)
if extResult == nil {
// Use address policy to determine deliverable mailboxes.
mailboxes = mailboxes[:0]
for _, recip := range recipients {
if recip.ShouldStore() {
mailboxes = append(mailboxes, recip.Mailbox)
}
}
inbound.Mailboxes = mailboxes
} else {
// Event response overrides destination mailboxes and address policy.
inbound = extResult
}
// Deliver to each mailbox.
for _, mb := range inbound.Mailboxes {
// Append recipient and timestamp to generated Received header.
recvd := fmt.Sprintf("%s for <%s>; %s\r\n", recvdHeader, mb, tstamp)
returnPath := fmt.Sprintf("Return-Path: <%s>\r\n", from.Address.Address)
// Deliver message.
logger.Debug().Str("mailbox", mb).Msg("Delivering message")
delivery := &Delivery{
Meta: event.MessageMetadata{
Mailbox: mb,
From: inbound.From,
To: inbound.To,
Date: now,
Subject: inbound.Subject,
Size: inbound.Size,
},
Reader: io.MultiReader(strings.NewReader(returnPath), strings.NewReader(recvd), bytes.NewReader(source)),
}
id, err := s.Store.AddMessage(delivery)
if err != nil {
logger.Error().Str("mailbox", mb).Err(err).Msg("Delivery failed")
return err
}
// Emit message stored event.
event := delivery.Meta
event.ID = id
s.ExtHost.Events.AfterMessageStored.Emit(&event)
}
return nil
}
// GetMetadata returns a slice of metadata for the specified mailbox.
func (s *StoreManager) GetMetadata(mailbox string) ([]*event.MessageMetadata, error) {
messages, err := s.Store.GetMessages(mailbox)
if err != nil {
return nil, err
}
metas := make([]*event.MessageMetadata, len(messages))
for i, sm := range messages {
metas[i] = MakeMetadata(sm)
}
return metas, nil
}
// GetMessage returns the specified message.
func (s *StoreManager) GetMessage(mailbox, id string) (*Message, error) {
sm, err := s.Store.GetMessage(mailbox, id)
if err != nil || sm == nil {
return nil, err
}
r, err := sm.Source()
if err != nil {
return nil, err
}
env, err := enmime.ReadEnvelope(r)
if err != nil {
return nil, err
}
_ = r.Close()
header := MakeMetadata(sm)
return &Message{MessageMetadata: *header, env: env}, nil
}
// MarkSeen marks the message as having been read.
func (s *StoreManager) MarkSeen(mailbox, id string) error {
log.Debug().Str("module", "manager").Str("mailbox", mailbox).Str("id", id).
Msg("Marking as seen")
return s.Store.MarkSeen(mailbox, id)
}
// PurgeMessages removes all messages from the specified mailbox.
func (s *StoreManager) PurgeMessages(mailbox string) error {
return s.Store.PurgeMessages(mailbox)
}
// RemoveMessage deletes the specified message.
func (s *StoreManager) RemoveMessage(mailbox, id string) error {
return s.Store.RemoveMessage(mailbox, id)
}
// SourceReader allows the stored message source to be read.
func (s *StoreManager) SourceReader(mailbox, id string) (io.ReadCloser, error) {
sm, err := s.Store.GetMessage(mailbox, id)
if err != nil || sm == nil {
return nil, err
}
return sm.Source()
}
// MailboxForAddress parses an email address to return the canonical mailbox name.
func (s *StoreManager) MailboxForAddress(mailbox string) (string, error) {
return s.AddrPolicy.ExtractMailbox(mailbox)
}
// MakeMetadata populates Metadata from a storage.Message.
func MakeMetadata(m storage.Message) *event.MessageMetadata {
return &event.MessageMetadata{
Mailbox: m.Mailbox(),
ID: m.ID(),
From: m.From(),
To: m.To(),
Date: m.Date(),
Subject: m.Subject(),
Size: m.Size(),
Seen: m.Seen(),
}
}

595
pkg/message/manager_test.go Normal file
View File

@@ -0,0 +1,595 @@
package message_test
import (
"fmt"
"io"
"net/mail"
"strings"
"testing"
"time"
"github.com/inbucket/inbucket/v3/pkg/config"
"github.com/inbucket/inbucket/v3/pkg/extension"
"github.com/inbucket/inbucket/v3/pkg/extension/event"
"github.com/inbucket/inbucket/v3/pkg/message"
"github.com/inbucket/inbucket/v3/pkg/policy"
"github.com/inbucket/inbucket/v3/pkg/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDeliverStoresMessages(t *testing.T) {
sm, _ := testStoreManager()
// Attempt to deliver a message to two mailboxes.
origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com")
recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com")
recip2, _ := sm.AddrPolicy.NewRecipient("u2@example.com")
err := sm.Deliver(
origin,
[]*policy.Recipient{recip1, recip2},
"Received: xyz\n",
[]byte(`From: from@example.com
To: u1@example.com, u2@example.com
Subject: tsub
test email`),
)
require.NoError(t, err)
assertMessageCount(t, sm, "u1@example.com", 1)
assertMessageCount(t, sm, "u2@example.com", 1)
}
func TestDeliverStoresMessageNoFromHeader(t *testing.T) {
sm, _ := testStoreManager()
// Attempt to deliver a message to two mailboxes.
origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com")
recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com")
recip2, _ := sm.AddrPolicy.NewRecipient("u2@example.com")
err := sm.Deliver(
origin,
[]*policy.Recipient{recip1, recip2},
"Received: xyz\n",
[]byte(`To: u1@example.com, u2@example.com
Subject: tsub
test email`),
)
require.NoError(t, err)
assertMessageCount(t, sm, "u1@example.com", 1)
assertMessageCount(t, sm, "u2@example.com", 1)
}
func TestDeliverStoresMessageNoToHeader(t *testing.T) {
sm, _ := testStoreManager()
// Attempt to deliver a message to two mailboxes.
origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com")
recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com")
recip2, _ := sm.AddrPolicy.NewRecipient("u2@example.com")
err := sm.Deliver(
origin,
[]*policy.Recipient{recip1, recip2},
"Received: xyz\n",
[]byte(`From: from@example.com
Subject: tsub
test email`),
)
require.NoError(t, err)
assertMessageCount(t, sm, "u1@example.com", 1)
assertMessageCount(t, sm, "u2@example.com", 1)
}
func TestDeliverRespectsRecipientPolicy(t *testing.T) {
sm, _ := testStoreManager()
// Attempt to deliver a message to two mailboxes.
origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com")
recip1, _ := sm.AddrPolicy.NewRecipient("u1@nostore.com")
recip2, _ := sm.AddrPolicy.NewRecipient("u2@example.com")
if err := sm.Deliver(
origin,
[]*policy.Recipient{recip1, recip2},
"Received: xyz\n",
[]byte("From: from@example.com\nSubject: tsub\n\ntest email"),
); err != nil {
t.Fatal(err)
}
// Expect empty mailbox for nostore domain.
assertMessageCount(t, sm, "u1@nostore.com", 0)
assertMessageCount(t, sm, "u2@example.com", 1)
}
func TestDeliverEmitsBeforeMessageStoredEventToHeader(t *testing.T) {
sm, extHost := testStoreManager()
// Register function to receive event.
var got *event.InboundMessage
extHost.Events.BeforeMessageStored.AddListener(
"test",
func(msg event.InboundMessage) *event.InboundMessage {
got = &msg
return nil
})
// Deliver a message to trigger event, To header differs from RCPT TO.
origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com")
recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com")
recip2, _ := sm.AddrPolicy.NewRecipient("u2@example.com")
if err := sm.Deliver(
origin,
[]*policy.Recipient{recip1, recip2},
"Received: xyz\n",
[]byte(`From: from@example.com
To: u1@example.com, u3@external.com
Subject: tsub
test email`),
); err != nil {
t.Fatal(err)
}
require.NotNil(t, got, "BeforeMessageStored listener did not receive InboundMessage")
assert.Equal(t, []string{"u1@example.com", "u2@example.com"}, got.Mailboxes, "Mailboxes not equal")
assert.Equal(t, &mail.Address{Name: "", Address: "from@example.com"}, got.From, "From not equal")
assert.Equal(t, []*mail.Address{
{Name: "", Address: "u1@example.com"},
{Name: "", Address: "u3@external.com"},
}, got.To, "To not equal")
assert.Equal(t, "tsub", got.Subject, "Subject not equal")
assert.Equal(t, int64(84), got.Size, "Size not equal")
}
func TestDeliverEmitsBeforeMessageStoredEventRcptTo(t *testing.T) {
sm, extHost := testStoreManager()
// Register function to receive event.
var got *event.InboundMessage
extHost.Events.BeforeMessageStored.AddListener(
"test",
func(msg event.InboundMessage) *event.InboundMessage {
got = &msg
return nil
})
// Deliver a message to trigger event, lacks To header.
origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com")
recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com")
recip2, _ := sm.AddrPolicy.NewRecipient("u2@example.com")
if err := sm.Deliver(
origin,
[]*policy.Recipient{recip1, recip2},
"Received: xyz\n",
[]byte("From: from@example.com\nSubject: tsub\n\ntest email"),
); err != nil {
t.Fatal(err)
}
require.NotNil(t, got, "BeforeMessageStored listener did not receive InboundMessage")
assert.Equal(t, []string{"u1@example.com", "u2@example.com"}, got.Mailboxes, "Mailboxes not equal")
assert.Equal(t, &mail.Address{Name: "", Address: "from@example.com"}, got.From, "From not equal")
assert.Equal(t, []*mail.Address{
{Name: "", Address: "u1@example.com"},
{Name: "", Address: "u2@example.com"},
}, got.To, "To not equal")
assert.Equal(t, "tsub", got.Subject, "Subject not equal")
assert.Equal(t, int64(48), got.Size, "Size not equal")
}
func TestDeliverUsesBeforeMessageStoredEventResponseMailboxes(t *testing.T) {
sm, extHost := testStoreManager()
// Register function to receive event.
extHost.Events.BeforeMessageStored.AddListener(
"test",
func(msg event.InboundMessage) *event.InboundMessage {
// Listener rewrites destination mailboxes.
resp := msg
resp.Mailboxes = []string{"new1@example.com", "new2@nostore.com"}
return &resp
})
// Deliver a message to trigger event.
origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com")
recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com")
recip2, _ := sm.AddrPolicy.NewRecipient("u2@example.com")
if err := sm.Deliver(
origin,
[]*policy.Recipient{recip1, recip2},
"Received: xyz\r\n",
[]byte("From: from@example.com\nSubject: tsub\n\ntest email"),
); err != nil {
t.Fatal(err)
}
// Expect messages in only the mailboxes in the event response, and for the DiscardDomains
// policy to be ignored for nostore.com.
assertMessageCount(t, sm, "u1@example.com", 0)
assertMessageCount(t, sm, "u2@example.com", 0)
assertMessageCount(t, sm, "new1@example.com", 1)
assertMessageCount(t, sm, "new2@nostore.com", 1)
}
func TestDeliverUsesBeforeMessageStoredEventResponseMailboxesEmpty(t *testing.T) {
sm, extHost := testStoreManager()
// Register function to receive event.
extHost.Events.BeforeMessageStored.AddListener(
"test",
func(msg event.InboundMessage) *event.InboundMessage {
// Listener clears destination mailboxes.
resp := msg
resp.Mailboxes = []string{}
return &resp
})
// Deliver a message to trigger event.
origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com")
recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com")
recip2, _ := sm.AddrPolicy.NewRecipient("u2@example.com")
if err := sm.Deliver(
origin,
[]*policy.Recipient{recip1, recip2},
"Received: xyz\r\n",
[]byte("From: from@example.com\nSubject: tsub\n\ntest email"),
); err != nil {
t.Fatal(err)
}
// Expect no messages the mailboxes.
assertMessageCount(t, sm, "u1@example.com", 0)
assertMessageCount(t, sm, "u2@example.com", 0)
}
func TestDeliverUsesBeforeMessageStoredEventResponseFields(t *testing.T) {
sm, extHost := testStoreManager()
// Register function to receive event.
extHost.Events.BeforeMessageStored.AddListener(
"test",
func(msg event.InboundMessage) *event.InboundMessage {
// Listener rewrites destination mailboxes.
msg.Subject = "event subj"
msg.From = &mail.Address{Address: "from@event.com", Name: "From Event"}
// Changing To does not affect destination mailbox(es).
msg.To = []*mail.Address{
{Address: "to@event.com", Name: "To Event"},
{Address: "to2@event.com", Name: "To 2 Event"},
}
// Size is read only, should have no effect.
msg.Size = 12345
return &msg
})
// Deliver a message to trigger event.
origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com")
recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com")
if err := sm.Deliver(
origin,
[]*policy.Recipient{recip1},
"Received: xyz\r\n",
[]byte("From: from@example.com\nSubject: tsub\n\ntest email"),
); err != nil {
t.Fatal(err)
}
// Verify single message stored.
metadata, err := sm.GetMetadata("u1@example.com")
require.NoError(t, err)
require.Len(t, metadata, 1, "mailbox has incorrect # of messages")
got := metadata[0]
// Verify metadata fields were overridden by event response values.
assert.Equal(t, "event subj", got.Subject, "Subject didn't match")
assert.Equal(t, "from@event.com", got.From.Address, "From Address didn't match")
assert.Equal(t, "From Event", got.From.Name, "From Name didn't match")
require.Len(t, got.To, 2)
assert.Equal(t, "to@event.com", got.To[0].Address, "To Address didn't match")
assert.Equal(t, "To Event", got.To[0].Name, "To Name didn't match")
assert.Equal(t, "to2@event.com", got.To[1].Address, "To Address didn't match")
assert.Equal(t, "To 2 Event", got.To[1].Name, "To Name didn't match")
assert.NotEqual(t, 12345, got.Size, "Size is read only")
}
func TestDeliverEmitsAfterMessageStoredEvent(t *testing.T) {
sm, extHost := testStoreManager()
listener := extHost.Events.AfterMessageStored.AsyncTestListener("manager", 1)
// Deliver a message to trigger event.
origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com")
recip, _ := sm.AddrPolicy.NewRecipient("to@example.com")
if err := sm.Deliver(
origin,
[]*policy.Recipient{recip},
"Received: xyz\n",
[]byte("From: from@example.com\nSubject: events\n\ntest email."),
); err != nil {
t.Fatal(err)
}
got, err := listener()
require.NoError(t, err)
assert.NotNil(t, got, "No event received, or it was nil")
assertMessageCount(t, sm, "to@example.com", 1)
// Verify event content.
assert.Equal(t, "to@example.com", got.Mailbox)
assert.Equal(t, "from@example.com", got.From.Address)
assert.WithinDuration(t, time.Now(), got.Date, 5*time.Second)
assert.Equal(t, "events", got.Subject, nil)
assert.Equal(t, int64(51), got.Size)
require.Len(t, got.To, 1)
assert.Equal(t, "to@example.com", got.To[0].Address)
}
func TestDeliverBeforeAndAfterMessageStoredEvents(t *testing.T) {
sm, extHost := testStoreManager()
// Register function to receive Before event.
extHost.Events.BeforeMessageStored.AddListener(
"test",
func(msg event.InboundMessage) *event.InboundMessage {
// Listener rewrites destination mailboxes.
resp := msg
resp.Mailboxes = []string{"new1@example.com", "new2@example.com"}
return &resp
})
// After event listener.
listener := extHost.Events.AfterMessageStored.AsyncTestListener("manager", 2)
// Deliver a message to trigger events.
origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com")
recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com")
recip2, _ := sm.AddrPolicy.NewRecipient("u2@example.com")
if err := sm.Deliver(
origin,
[]*policy.Recipient{recip1, recip2},
"Received: xyz\r\n",
[]byte("From: from@example.com\nSubject: tsub\n\ntest email"),
); err != nil {
t.Fatal(err)
}
// Confirm mailbox names overridden by `Before` were sent to `After` event. Order is
// not guaranteed.
got1, err := listener()
require.NoError(t, err)
got2, err := listener()
require.NoError(t, err)
got := []string{got1.Mailbox, got2.Mailbox}
assert.Contains(t, got, "new1@example.com")
assert.Contains(t, got, "new2@example.com")
}
func TestGetMessage(t *testing.T) {
sm, _ := testStoreManager()
// Add a test message.
subject := "getMessage1"
id := addTestMessage(sm, "get-box", subject)
// Verify retrieval of the test message.
msg, err := sm.GetMessage("get-box", id)
require.NoError(t, err, "GetMessage must succeed")
require.NotNil(t, msg, "GetMessage must return a result")
assert.Equal(t, subject, msg.Subject)
assert.Contains(t, msg.Text(), fmt.Sprintf("about %q", subject))
}
func TestMarkSeen(t *testing.T) {
sm, _ := testStoreManager()
// Add a test message.
subject := "getMessage1"
id := addTestMessage(sm, "seen-box", subject)
// Verify test message unseen.
msg, err := sm.GetMessage("seen-box", id)
require.NoError(t, err, "GetMessage must succeed")
require.NotNil(t, msg, "GetMessage must return a result")
assert.False(t, msg.Seen, "msg should be unseen")
err = sm.MarkSeen("seen-box", id)
require.NoError(t, err, "MarkSeen should succeed")
// Verify test message seen.
msg, err = sm.GetMessage("seen-box", id)
require.NoError(t, err, "GetMessage must succeed")
require.NotNil(t, msg, "GetMessage must return a result")
assert.True(t, msg.Seen, "msg should have been seen")
}
func TestRemoveMessage(t *testing.T) {
sm, _ := testStoreManager()
// Add test messages.
id1 := addTestMessage(sm, "rm-box", "subject 1")
id2 := addTestMessage(sm, "rm-box", "subject 2")
id3 := addTestMessage(sm, "rm-box", "subject 3")
got, err := sm.GetMetadata("rm-box")
require.NoError(t, err)
require.Len(t, got, 3)
// Delete message 2 and verify.
err = sm.RemoveMessage("rm-box", id2)
require.NoError(t, err)
got, err = sm.GetMetadata("rm-box")
require.NoError(t, err)
require.Len(t, got, 2, "Should be 2 messages remaining")
gotIDs := make([]string, 0, 3)
for _, msg := range got {
gotIDs = append(gotIDs, msg.ID)
}
assert.Contains(t, gotIDs, id1)
assert.Contains(t, gotIDs, id3)
}
func TestPurgeMessages(t *testing.T) {
sm, _ := testStoreManager()
// Add test messages.
_ = addTestMessage(sm, "purge-box", "subject 1")
_ = addTestMessage(sm, "purge-box", "subject 2")
_ = addTestMessage(sm, "purge-box", "subject 3")
got, err := sm.GetMetadata("purge-box")
require.NoError(t, err)
require.Len(t, got, 3)
// Purge and verify.
err = sm.PurgeMessages("purge-box")
require.NoError(t, err)
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) {
sm, _ := testStoreManager()
recvdHeader := "Received: xyz\n"
msgSource := `From: from@example.com
To: u1@example.com, u2@example.com
Subject: tsub
test email`
// Deliver mesage.
origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com")
recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com")
err := sm.Deliver(origin, []*policy.Recipient{recip1}, recvdHeader, []byte(msgSource))
require.NoError(t, err)
// Find message ID.
msgs, err := sm.GetMetadata("u1@example.com")
require.NoError(t, err, "Failed to read mailbox")
require.Len(t, msgs, 1, "Unexpected mailbox len")
id := msgs[0].ID
// Read back and verify source.
r, err := sm.SourceReader("u1@example.com", id)
require.NoError(t, err, "SourceReader must succeed")
gotBytes, err := io.ReadAll(r)
require.NoError(t, err, "Failed to read source")
got := string(gotBytes)
assert.Contains(t, got, recvdHeader, "Source should contain received header")
assert.Contains(t, got, msgSource, "Source should contain original message source")
}
func TestMailboxForAddress(t *testing.T) {
// Configured for FullNaming.
sm, _ := testStoreManager()
addr := "u1@example.com"
got, err := sm.MailboxForAddress(addr)
require.NoError(t, err)
assert.Equal(t, addr, got, "FullNaming mode should return a full address for mailbox")
}
func TestReturnPath(t *testing.T) {
sm, _ := testStoreManager()
recvdHeader := "Received: xyz\n"
msgSource := `From: from@example.com
To: u1@example.com
Subject: return path
test email`
// Deliver message.
origin, _ := sm.AddrPolicy.ParseOrigin("821from@example.com")
recipient, _ := sm.AddrPolicy.NewRecipient("u1@example.com")
err := sm.Deliver(origin, []*policy.Recipient{recipient}, recvdHeader, []byte(msgSource))
require.NoError(t, err)
// Find message ID.
msgs, err := sm.GetMetadata("u1@example.com")
require.NoError(t, err, "Failed to read mailbox")
require.Len(t, msgs, 1, "Unexpected mailbox len")
id := msgs[0].ID
// Read back and verify source.
r, err := sm.SourceReader("u1@example.com", id)
require.NoError(t, err, "SourceReader must succeed")
gotBytes, err := io.ReadAll(r)
require.NoError(t, err, "Failed to read source")
got := string(gotBytes)
assert.Contains(t, got, "Return-Path: <821from@example.com>\r\n", "Source should contain return-path")
}
// Returns an empty StoreManager and extension Host pair, configured for testing.
func testStoreManager() (*message.StoreManager, *extension.Host) {
extHost := extension.NewHost()
sm := &message.StoreManager{
AddrPolicy: &policy.Addressing{
Config: &config.Root{
MailboxNaming: config.FullNaming,
SMTP: config.SMTP{
DefaultAccept: true,
DefaultStore: true,
RejectDomains: []string{"noaccept.com"},
DiscardDomains: []string{"nostore.com"},
},
},
},
Store: test.NewStore(),
ExtHost: extHost,
}
return sm, extHost
}
// Adds a test message to the provided store, returning the new message ID.
func addTestMessage(sm *message.StoreManager, mailbox string, subject string) string {
from := mail.Address{Name: "From Test", Address: "from@example.com"}
to := mail.Address{Name: "To Test", Address: "to@example.com"}
delivery := &message.Delivery{
Meta: event.MessageMetadata{
Mailbox: mailbox,
From: &from,
To: []*mail.Address{&to},
Date: time.Now(),
Subject: subject,
},
Reader: strings.NewReader(fmt.Sprintf(
"From: %s\nTo: %s\nSubject: %s\n\nTest message about %q\n",
from, to, subject, subject,
)),
}
id, err := sm.Store.AddMessage(delivery)
if err != nil {
panic(err)
}
return id
}
func assertMessageCount(t *testing.T, sm *message.StoreManager, mailbox string, count int) {
t.Helper()
metas, err := sm.GetMetadata(mailbox)
require.NoError(t, err, "StoreManager GetMetadata failed")
got := len(metas)
if got != count {
t.Errorf("Mailbox %q got %v messages, wanted %v", mailbox, got, count)
}
}

107
pkg/message/message.go Normal file
View File

@@ -0,0 +1,107 @@
// Package message contains message handling logic.
package message
import (
"io"
"net/mail"
"net/textproto"
"time"
"github.com/inbucket/inbucket/v3/pkg/extension/event"
"github.com/inbucket/inbucket/v3/pkg/storage"
"github.com/jhillyerd/enmime/v2"
)
// Message holds both the metadata and content of a message.
type Message struct {
event.MessageMetadata
env *enmime.Envelope
}
// New constructs a new Message
func New(m event.MessageMetadata, e *enmime.Envelope) *Message {
return &Message{
MessageMetadata: m,
env: e,
}
}
// Attachments returns the MIME attachments for the message.
func (m *Message) Attachments() []*enmime.Part {
attachments := append([]*enmime.Part{}, m.env.Inlines...)
attachments = append(attachments, m.env.Attachments...)
return attachments
}
// Header returns the header map for this message.
func (m *Message) Header() textproto.MIMEHeader {
return m.env.Root.Header
}
// HTML returns the HTML body of the message.
func (m *Message) HTML() string {
return m.env.HTML
}
// MIMEErrors returns MIME parsing errors and warnings.
func (m *Message) MIMEErrors() []*enmime.Error {
return m.env.Errors
}
// Text returns the plain text body of the message.
func (m *Message) Text() string {
return m.env.Text
}
// Delivery is used to add a message to storage.
type Delivery struct {
Meta event.MessageMetadata
Reader io.Reader
}
var _ storage.Message = &Delivery{}
// Mailbox getter.
func (d *Delivery) Mailbox() string {
return d.Meta.Mailbox
}
// ID getter.
func (d *Delivery) ID() string {
return d.Meta.ID
}
// From getter.
func (d *Delivery) From() *mail.Address {
return d.Meta.From
}
// To getter.
func (d *Delivery) To() []*mail.Address {
return d.Meta.To
}
// Date getter.
func (d *Delivery) Date() time.Time {
return d.Meta.Date
}
// Subject getter.
func (d *Delivery) Subject() string {
return d.Meta.Subject
}
// Size getter.
func (d *Delivery) Size() int64 {
return d.Meta.Size
}
// Source contains the raw content of the message.
func (d *Delivery) Source() (io.ReadCloser, error) {
return io.NopCloser(d.Reader), nil
}
// Seen getter.
func (d *Delivery) Seen() bool {
return d.Meta.Seen
}

63
pkg/metric/metric.go Normal file
View File

@@ -0,0 +1,63 @@
package metric
import (
"container/list"
"expvar"
"strings"
"time"
)
// TickerFunc is the function signature accepted by AddTickerFunc, will be called once per minute.
type TickerFunc func()
var tickerFuncChan = make(chan TickerFunc)
func init() {
go metricsTicker()
}
// AddTickerFunc adds a new function callback to the list of metrics TickerFuncs that get
// called each minute.
func AddTickerFunc(f TickerFunc) {
tickerFuncChan <- f
}
// Push adds the metric to the end of the list and returns a comma separated string of the
// previous 61 entries. We return 61 instead of 60 (an hour) because the chart on the client
// tracks deltas between these values - there is nothing to compare the first value against.
func Push(history *list.List, ev expvar.Var) string {
history.PushBack(ev.String())
if history.Len() > 61 {
history.Remove(history.Front())
}
return joinStringList(history)
}
// metricsTicker calls the current list of TickerFuncs once per minute.
func metricsTicker() {
funcs := make([]TickerFunc, 0)
ticker := time.NewTicker(time.Minute)
for {
select {
case <-ticker.C:
for _, f := range funcs {
f()
}
case f := <-tickerFuncChan:
funcs = append(funcs, f)
}
}
}
// joinStringList joins a List containing strings by commas.
func joinStringList(listOfStrings *list.List) string {
if listOfStrings.Len() == 0 {
return ""
}
s := make([]string, 0, listOfStrings.Len())
for e := listOfStrings.Front(); e != nil; e = e.Next() {
s = append(s, e.Value.(string))
}
return strings.Join(s, ",")
}

161
pkg/msghub/hub.go Normal file
View File

@@ -0,0 +1,161 @@
package msghub
import (
"container/ring"
"context"
"github.com/inbucket/inbucket/v3/pkg/extension"
"github.com/inbucket/inbucket/v3/pkg/extension/event"
"github.com/rs/zerolog/log"
)
// Length of msghub operation queue
const opChanLen = 100
// Listener receives the contents of the history buffer, followed by new messages
type Listener interface {
Receive(msg event.MessageMetadata) error
Delete(mailbox string, id string) error
}
// Hub relays messages on to its listeners
type Hub struct {
// history buffer, points next Message to write. Proceeding non-nil entry is oldest Message
history *ring.Ring
listeners map[Listener]struct{} // listeners interested in new messages
opChan chan func(h *Hub) // operations queued for this actor
}
// New constructs a new Hub which will cache historyLen messages in memory for playback to future
// listeners. A goroutine is created to handle incoming messages; it will run until the provided
// context is canceled.
func New(historyLen int, extHost *extension.Host) *Hub {
hub := &Hub{
history: ring.New(historyLen),
listeners: make(map[Listener]struct{}),
opChan: make(chan func(h *Hub), opChanLen),
}
// Register an extension event listener for MessageStored.
extHost.Events.AfterMessageStored.AddListener("msghub",
func(msg event.MessageMetadata) {
hub.Dispatch(msg)
})
extHost.Events.AfterMessageDeleted.AddListener("msghub",
func(msg event.MessageMetadata) {
hub.Delete(msg.Mailbox, msg.ID)
})
return hub
}
// Start Hub processing loop.
func (hub *Hub) Start(ctx context.Context) {
for {
select {
case <-ctx.Done():
// Shutdown
close(hub.opChan)
return
case op := <-hub.opChan:
hub.runOp(op)
}
}
}
// Dispatch queues a message for broadcast by the hub. The message will be placed into the
// history buffer and then relayed to all registered listeners.
func (hub *Hub) Dispatch(msg event.MessageMetadata) {
hub.opChan <- func(h *Hub) {
if h.history != nil {
// Add to history buffer
h.history.Value = msg
h.history = h.history.Next()
// Relay event to all listeners, removing listeners if they return an error.
for l := range h.listeners {
if err := l.Receive(msg); err != nil {
delete(h.listeners, l)
}
}
}
}
}
// Delete removes the message from the history buffer and instructs listeners to do the same.
func (hub *Hub) Delete(mailbox string, id string) {
hub.opChan <- func(h *Hub) {
if h.history == nil {
return
}
// Locate and remove history entry.
p := h.history
end := p
for {
if next, ok := p.Next().Value.(event.MessageMetadata); ok {
if mailbox == next.Mailbox && id == next.ID {
p.Next().Value = nil
break
}
}
if p = p.Next(); p == end {
break
}
}
// Relay event to all listeners, removing listeners if they return an error.
for l := range h.listeners {
if err := l.Delete(mailbox, id); err != nil {
delete(h.listeners, l)
}
}
}
}
// AddListener registers a listener to receive broadcasted messages.
func (hub *Hub) AddListener(l Listener) {
hub.opChan <- func(h *Hub) {
// Playback log
h.history.Do(func(v interface{}) {
if v != nil {
_ = l.Receive(v.(event.MessageMetadata))
}
})
// Add to listeners
h.listeners[l] = struct{}{}
}
}
// RemoveListener deletes a listener registration, it will cease to receive messages.
func (hub *Hub) RemoveListener(l Listener) {
hub.opChan <- func(h *Hub) {
delete(h.listeners, l)
}
}
// Sync blocks until the msghub has processed its queue up to this point, useful
// for unit tests.
func (hub *Hub) Sync() {
done := make(chan struct{})
hub.opChan <- func(_ *Hub) {
close(done)
}
<-done
}
func (hub *Hub) runOp(op func(*Hub)) {
defer func() {
if r := recover(); r != nil {
if err, ok := r.(error); ok {
log.Error().Str("module", "msghub").Err(err).Msg("Operation panicked")
} else {
log.Error().Str("module", "msghub").Err(err).Msgf("Operation panicked: %s", r)
}
}
}()
op(hub)
}

380
pkg/msghub/hub_test.go Normal file
View File

@@ -0,0 +1,380 @@
package msghub
import (
"context"
"errors"
"fmt"
"strconv"
"testing"
"time"
"github.com/inbucket/inbucket/v3/pkg/extension"
"github.com/inbucket/inbucket/v3/pkg/extension/event"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testListener implements the Listener interface, mock for unit tests
type testListener struct {
messages []*event.MessageMetadata // received messages
deletes []string // received deletes
wantEvents int // how many events this listener wants to receive
errorAfter int // when != 0, event count until Receive() begins returning error
gotEvents int
done chan struct{} // closed once we have received wantMessages
overflow chan struct{} // closed if we receive wantMessages+1
}
func newTestListener(want int) *testListener {
l := &testListener{
messages: make([]*event.MessageMetadata, 0, want*2),
deletes: make([]string, 0, want*2),
wantEvents: want,
done: make(chan struct{}),
overflow: make(chan struct{}),
}
if want == 0 {
close(l.done)
}
return l
}
// Receive a Message, store it in the messages slice, close applicable channels, and return an error
// if instructed
func (l *testListener) Receive(msg event.MessageMetadata) error {
l.gotEvents++
l.messages = append(l.messages, &msg)
if l.gotEvents == l.wantEvents {
close(l.done)
}
if l.gotEvents == l.wantEvents+1 {
close(l.overflow)
}
if l.errorAfter > 0 && l.gotEvents > l.errorAfter {
return errors.New("too many messages")
}
return nil
}
func (l *testListener) Delete(mailbox string, id string) error {
l.gotEvents++
l.deletes = append(l.deletes, mailbox+"/"+id)
return nil
}
// String formats the got vs wanted message counts
func (l *testListener) String() string {
return fmt.Sprintf("got %v messages, wanted %v", len(l.messages), l.wantEvents)
}
func TestHubNew(t *testing.T) {
hub := New(5, extension.NewHost())
if hub == nil {
t.Fatal("New() == nil, expected a new Hub")
}
}
func TestHubZeroLen(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
hub := New(0, extension.NewHost())
go hub.Start(ctx)
m := event.MessageMetadata{}
for range 100 {
hub.Dispatch(m)
}
// Ensures Hub doesn't panic
}
func TestHubZeroListeners(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
hub := New(5, extension.NewHost())
go hub.Start(ctx)
m := event.MessageMetadata{}
for range 100 {
hub.Dispatch(m)
}
// Ensures Hub doesn't panic
}
func TestHubOneListener(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
hub := New(5, extension.NewHost())
go hub.Start(ctx)
m := event.MessageMetadata{}
l := newTestListener(1)
hub.AddListener(l)
hub.Dispatch(m)
// Wait for messages
select {
case <-l.done:
case <-time.After(time.Second):
t.Error("Timeout:", l)
}
}
func TestHubRemoveListener(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
hub := New(5, extension.NewHost())
go hub.Start(ctx)
m := event.MessageMetadata{}
l := newTestListener(1)
hub.AddListener(l)
hub.Dispatch(m)
hub.RemoveListener(l)
hub.Dispatch(m)
hub.Sync()
// Wait for messages
select {
case <-l.overflow:
t.Error(l)
case <-time.After(50 * time.Millisecond):
// Expected result, no overflow
}
}
func TestHubRemoveListenerOnError(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
hub := New(5, extension.NewHost())
go hub.Start(ctx)
m := event.MessageMetadata{}
// error after 1 means listener should receive 2 messages before being removed
l := newTestListener(2)
l.errorAfter = 1
hub.AddListener(l)
hub.Dispatch(m)
hub.Dispatch(m)
hub.Dispatch(m)
hub.Dispatch(m)
hub.Sync()
// Wait for messages
select {
case <-l.overflow:
t.Error(l)
case <-time.After(50 * time.Millisecond):
// Expected result, no overflow
}
}
func TestHubHistoryReplay(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
hub := New(100, extension.NewHost())
go hub.Start(ctx)
l1 := newTestListener(3)
hub.AddListener(l1)
// Broadcast 3 messages with no listeners
msgs := make([]event.MessageMetadata, 3)
for i := range msgs {
msgs[i] = event.MessageMetadata{
Subject: fmt.Sprintf("subj %v", i),
}
hub.Dispatch(msgs[i])
}
// Wait for messages (live)
select {
case <-l1.done:
case <-time.After(time.Second):
t.Fatal("Timeout:", l1)
}
// Add a new listener
l2 := newTestListener(3)
hub.AddListener(l2)
// Wait for messages (history)
select {
case <-l2.done:
case <-time.After(time.Second):
t.Fatal("Timeout:", l2)
}
for i := range msgs {
got := l2.messages[i].Subject
want := msgs[i].Subject
if got != want {
t.Errorf("msg[%v].Subject == %q, want %q", i, got, want)
}
}
}
func TestHubHistoryDelete(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
hub := New(100, extension.NewHost())
go hub.Start(ctx)
l1 := newTestListener(3)
hub.AddListener(l1)
// Broadcast 3 messages with no listeners
msgs := make([]event.MessageMetadata, 3)
for i := range msgs {
msgs[i] = event.MessageMetadata{
Mailbox: "hub",
ID: strconv.Itoa(i),
Subject: fmt.Sprintf("subj %v", i),
}
hub.Dispatch(msgs[i])
}
// Wait for messages (live)
select {
case <-l1.done:
case <-time.After(time.Second):
t.Fatal("Timeout:", l1)
}
hub.Delete("hub", "1") // Delete a message
hub.Delete("zzz", "0") // Attempt to delete non-existent mailbox message
// Add a new listener, waits for 2 messages
l2 := newTestListener(2)
hub.AddListener(l2)
// Wait for messages (history)
select {
case <-l2.done:
case <-time.After(time.Second):
t.Fatal("Timeout:", l2)
}
want := []string{"subj 0", "subj 2"}
for i := range want {
got := l2.messages[i].Subject
if got != want[i] {
t.Errorf("msg[%v].Subject == %q, want %q", i, got, want[i])
}
}
}
func TestHubHistoryReplayWrap(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
hub := New(5, extension.NewHost())
go hub.Start(ctx)
l1 := newTestListener(20)
hub.AddListener(l1)
// Broadcast more messages than the hub can hold
msgs := make([]event.MessageMetadata, 20)
for i := range msgs {
msgs[i] = event.MessageMetadata{
Subject: fmt.Sprintf("subj %v", i),
}
hub.Dispatch(msgs[i])
}
// Wait for messages (live)
select {
case <-l1.done:
case <-time.After(time.Second):
t.Fatal("Timeout:", l1)
}
// Add a new listener
l2 := newTestListener(5)
hub.AddListener(l2)
// Wait for messages (history)
select {
case <-l2.done:
case <-time.After(time.Second):
t.Fatal("Timeout:", l2)
}
for i := range 5 {
got := l2.messages[i].Subject
want := msgs[i+15].Subject
if got != want {
t.Errorf("msg[%v].Subject == %q, want %q", i, got, want)
}
}
}
func TestHubHistoryReplayWrapAfterDelete(t *testing.T) {
bufferSize := 5
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
hub := New(bufferSize, extension.NewHost())
go hub.Start(ctx)
waitForMessages := func(n int) {
l := newTestListener(n)
hub.AddListener(l)
select {
case <-l.done:
case <-time.After(time.Second):
t.Fatal("Timeout:", l)
}
}
// Broadcast more messages than the hub can hold.
msgs := make([]event.MessageMetadata, 10)
for i := range msgs {
msgs[i] = event.MessageMetadata{
Mailbox: "first",
ID: strconv.Itoa(i),
Subject: fmt.Sprintf("subj %v", i),
}
hub.Dispatch(msgs[i])
}
waitForMessages(bufferSize)
// Buffer must be configured size.
require.Equal(t, bufferSize, hub.history.Len())
// Delete a message still present in buffer.
hub.Delete("first", "7")
// Broadcast another set of messages.
for i := range msgs {
msgs[i] = event.MessageMetadata{
Mailbox: "second",
ID: strconv.Itoa(i),
Subject: fmt.Sprintf("subj %v", i),
}
hub.Dispatch(msgs[i])
}
waitForMessages(bufferSize)
// Ensure the buffer did not shrink after delete.
got := hub.history.Len()
assert.Equal(t, bufferSize, got, "got buffer size %d, wanted %d", got, bufferSize)
}
func TestHubContextCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
hub := New(5, extension.NewHost())
go hub.Start(ctx)
m := event.MessageMetadata{}
l := newTestListener(1)
hub.AddListener(l)
hub.Dispatch(m)
hub.Sync()
cancel()
// Wait for messages
select {
case <-l.overflow:
t.Error(l)
case <-time.After(50 * time.Millisecond):
// Expected result, no overflow
}
}

400
pkg/policy/address.go Normal file
View File

@@ -0,0 +1,400 @@
package policy
import (
"bytes"
"errors"
"fmt"
"net"
"net/mail"
"strings"
"github.com/inbucket/inbucket/v3/pkg/config"
"github.com/inbucket/inbucket/v3/pkg/stringutil"
)
// Addressing handles email address policy.
type Addressing struct {
Config *config.Root
}
// ExtractMailbox extracts the mailbox name from a partial email address.
func (a *Addressing) ExtractMailbox(address string) (string, error) {
if a.Config.MailboxNaming == config.DomainNaming {
return extractDomainMailbox(address)
}
local, domain, err := parseEmailAddress(address)
if err != nil {
return "", err
}
local, err = parseMailboxName(local)
if err != nil {
return "", err
}
if a.Config.MailboxNaming == config.LocalNaming {
return local, nil
}
if a.Config.MailboxNaming != config.FullNaming {
return "", fmt.Errorf("unknown MailboxNaming value: %v", a.Config.MailboxNaming)
}
if domain == "" {
return local, nil
}
if !ValidateDomainPart(domain) {
return "", fmt.Errorf("domain part %q in %q failed validation", domain, address)
}
return local + "@" + domain, nil
}
// NewRecipient parses an address into a Recipient. This is used for parsing RCPT TO arguments,
// not To headers.
func (a *Addressing) NewRecipient(address string) (*Recipient, error) {
local, domain, err := ParseEmailAddress(address)
if err != nil {
return nil, err
}
mailbox, err := a.ExtractMailbox(address)
if err != nil {
return nil, err
}
return &Recipient{
Address: mail.Address{Address: address},
addrPolicy: a,
LocalPart: local,
Domain: domain,
Mailbox: mailbox,
}, nil
}
// ParseOrigin parses an address into a Origin. This is used for parsing MAIL FROM argument,
// not To headers.
func (a *Addressing) ParseOrigin(address string) (*Origin, error) {
if address == "" {
return &Origin{
addrPolicy: a,
}, nil
}
local, domain, err := ParseEmailAddress(address)
if err != nil {
return nil, err
}
return &Origin{
Address: mail.Address{Address: address},
addrPolicy: a,
LocalPart: local,
Domain: domain,
}, nil
}
// ShouldAcceptDomain indicates if Inbucket accepts mail destined for the specified domain.
func (a *Addressing) ShouldAcceptDomain(domain string) bool {
domain = strings.ToLower(domain)
if a.Config.SMTP.DefaultAccept &&
!stringutil.SliceContains(a.Config.SMTP.RejectDomains, domain) {
return true
}
if !a.Config.SMTP.DefaultAccept &&
stringutil.SliceContains(a.Config.SMTP.AcceptDomains, domain) {
return true
}
return false
}
// ShouldStoreDomain indicates if Inbucket stores mail destined for the specified domain.
func (a *Addressing) ShouldStoreDomain(domain string) bool {
domain = strings.ToLower(domain)
if a.Config.SMTP.DefaultStore &&
!stringutil.SliceContains(a.Config.SMTP.DiscardDomains, domain) {
return true
}
if !a.Config.SMTP.DefaultStore &&
stringutil.SliceContains(a.Config.SMTP.StoreDomains, domain) {
return true
}
return false
}
// ShouldAcceptOriginDomain indicates if Inbucket accept mail from the specified domain.
func (a *Addressing) ShouldAcceptOriginDomain(domain string) bool {
domain = strings.ToLower(domain)
if len(a.Config.SMTP.RejectOriginDomains) > 0 {
for _, d := range a.Config.SMTP.RejectOriginDomains {
if stringutil.MatchWithWildcards(d, domain) {
return false
}
}
}
return true
}
// ParseEmailAddress unescapes an email address, and splits the local part from the domain part.
// An error is returned if the local or domain parts fail validation following the guidelines
// in RFC3696.
func ParseEmailAddress(address string) (local string, domain string, err error) {
local, domain, err = parseEmailAddress(address)
if err != nil {
return "", "", err
}
if !ValidateDomainPart(domain) {
return "", "", errors.New("domain part validation failed")
}
return local, domain, nil
}
// ValidateDomainPart returns true if the domain part complies to RFC3696, RFC1035. Used by
// ParseEmailAddress().
func ValidateDomainPart(domain string) bool {
ln := len(domain)
if ln == 0 {
return false
}
if ln > 255 {
return false
}
if ln >= 4 && domain[0] == '[' && domain[ln-1] == ']' {
// Bracketed domains must contain an IP address.
s := 1
if strings.HasPrefix(domain[1:], "IPv6:") {
s = 6
}
ip := net.ParseIP(domain[s : ln-1])
return ip != nil
}
if domain[ln-1] != '.' {
domain += "."
}
prev := '.'
labelLen := 0
hasAlphaNum := false
for _, c := range domain {
switch {
case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
('0' <= c && c <= '9') || c == '_':
// Must contain some of these to be a valid label.
hasAlphaNum = true
labelLen++
case c == '-':
if prev == '.' || prev == '-' {
// Cannot lead with hyphen or double hyphen.
return false
}
case c == '.':
if prev == '.' || prev == '-' {
// Cannot end with hyphen or double-dot.
return false
}
if labelLen > 63 {
return false
}
if !hasAlphaNum {
return false
}
labelLen = 0
hasAlphaNum = false
default:
// Unknown character.
return false
}
prev = c
}
return true
}
// Extracts the mailbox name when domain addressing is enabled.
func extractDomainMailbox(address string) (string, error) {
var local, domain string
var err error
if address != "" && address[0] == '[' && address[len(address)-1] == ']' {
// Likely an IP address in brackets, treat as domain only.
domain = address
} else {
local, domain, err = parseEmailAddress(address)
if err != nil {
return "", err
}
}
if local != "" {
local, err = parseMailboxName(local)
if err != nil {
return "", err
}
}
// If no @domain is specified, assume this is being used for mailbox lookup via the API.
if domain == "" {
domain = local
}
if !ValidateDomainPart(domain) {
return "", fmt.Errorf("domain part %q in %q failed validation", domain, address)
}
return domain, nil
}
// parseEmailAddress unescapes an email address, and splits the local part from the domain part. An
// error is returned if the local part fails validation following the guidelines in RFC3696. The
// domain part is optional and not validated.
func parseEmailAddress(address string) (local string, domain string, err error) {
if address == "" {
return "", "", errors.New("empty address")
}
if len(address) > 320 {
return "", "", errors.New("address exceeds 320 characters")
}
// Remove forward-path routes.
if address[0] == '@' {
end := strings.IndexRune(address, ':')
if end == -1 {
return "", "", errors.New("missing terminating ':' in route specification")
}
address = address[end+1:]
if address == "" {
return "", "", errors.New("address empty after removing route specification")
}
}
if address[0] == '.' {
return "", "", errors.New("address cannot start with a period")
}
// Loop over address parsing out local part.
buf := new(bytes.Buffer)
prev := byte('.')
inCharQuote := false
inStringQuote := false
LOOP:
for i := range len(address) {
c := address[i]
switch {
case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'):
// Letters are OK.
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
case '0' <= c && c <= '9':
// Numbers are OK.
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
case strings.IndexByte("!#$%&'*+-/=?^_`{|}~", c) >= 0:
// These specials can be used unquoted.
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
case c == '.':
// A single period is OK.
if prev == '.' {
// Sequence of periods is not permitted.
return "", "", errors.New("sequence of periods is not permitted")
}
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
case c == '\\':
inCharQuote = true
case c == '"':
switch {
case inCharQuote:
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
case inStringQuote:
inStringQuote = false
default:
if i == 0 {
inStringQuote = true
} else {
return "", "", errors.New("quoted string can only begin at start of address")
}
}
case c == '@':
if inCharQuote || inStringQuote {
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
} else {
// End of local-part.
if i > 128 {
return "", "", errors.New("local part must not exceed 128 characters")
}
if prev == '.' {
return "", "", errors.New("local part cannot end with a period")
}
domain = address[i+1:]
break LOOP
}
case c > 127:
return "", "", errors.New("characters outside of US-ASCII range not permitted")
default:
if inCharQuote || inStringQuote {
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
} else {
return "", "", fmt.Errorf("character %q must be quoted", c)
}
}
prev = c
}
if inCharQuote {
return "", "", errors.New("cannot end address with unterminated quoted-pair")
}
if inStringQuote {
return "", "", errors.New("cannot end address with unterminated string quote")
}
return buf.String(), domain, nil
}
// ParseMailboxName takes a localPart string (ex: "user+ext" without "@domain")
// and returns just the mailbox name (ex: "user"). Returns an error if
// localPart contains invalid characters; it won't accept any that must be
// quoted according to RFC3696.
func parseMailboxName(localPart string) (result string, err error) {
if localPart == "" {
return "", errors.New("mailbox name cannot be empty")
}
result = strings.ToLower(localPart)
invalid := make([]byte, 0, 10)
for i := range len(result) {
c := result[i]
switch {
case 'a' <= c && c <= 'z':
case '0' <= c && c <= '9':
case strings.IndexByte("!#$%&'*+-=/?^_`.{|}~", c) >= 0:
default:
invalid = append(invalid, c)
}
}
if len(invalid) > 0 {
return "", fmt.Errorf("mailbox name contained invalid character(s): %q", invalid)
}
if idx := strings.Index(result, "+"); idx > -1 {
result = result[0:idx]
}
return result, nil
}

515
pkg/policy/address_test.go Normal file
View File

@@ -0,0 +1,515 @@
package policy_test
import (
"strings"
"testing"
"github.com/inbucket/inbucket/v3/pkg/config"
"github.com/inbucket/inbucket/v3/pkg/policy"
)
func TestShouldAcceptDomain(t *testing.T) {
// Test with default accept.
ap := &policy.Addressing{
Config: &config.Root{
SMTP: config.SMTP{
DefaultAccept: true,
RejectDomains: []string{"a.deny.com", "deny.com"},
},
},
}
testCases := []struct {
domain string
want bool
}{
{domain: "bar.com", want: true},
{domain: "DENY.com", want: false},
{domain: "a.deny.com", want: false},
{domain: "b.deny.com", want: true},
}
for _, tc := range testCases {
t.Run(tc.domain, func(t *testing.T) {
got := ap.ShouldAcceptDomain(tc.domain)
if got != tc.want {
t.Errorf("Got %v for %q, want: %v", got, tc.domain, tc.want)
}
})
}
// Test with default reject.
ap = &policy.Addressing{
Config: &config.Root{
SMTP: config.SMTP{
DefaultAccept: false,
AcceptDomains: []string{"a.allow.com", "allow.com"},
},
},
}
testCases = []struct {
domain string
want bool
}{
{domain: "bar.com", want: false},
{domain: "ALLOW.com", want: true},
{domain: "a.allow.com", want: true},
{domain: "b.allow.com", want: false},
}
for _, tc := range testCases {
t.Run(tc.domain, func(t *testing.T) {
got := ap.ShouldAcceptDomain(tc.domain)
if got != tc.want {
t.Errorf("Got %v for %q, want: %v", got, tc.domain, tc.want)
}
})
}
}
func TestShouldStoreDomain(t *testing.T) {
// Test with storage enabled.
ap := &policy.Addressing{
Config: &config.Root{
SMTP: config.SMTP{
DefaultStore: false,
StoreDomains: []string{"store.com", "a.store.com"},
},
},
}
testCases := []struct {
domain string
want bool
}{
{domain: "foo.com", want: false},
{domain: "STORE.com", want: true},
{domain: "a.store.com", want: true},
{domain: "b.store.com", want: false},
}
for _, tc := range testCases {
t.Run(tc.domain, func(t *testing.T) {
got := ap.ShouldStoreDomain(tc.domain)
if got != tc.want {
t.Errorf("Got store %v for %q, want: %v", got, tc.domain, tc.want)
}
})
}
// Test with storage disabled.
ap = &policy.Addressing{
Config: &config.Root{
SMTP: config.SMTP{
DefaultStore: true,
DiscardDomains: []string{"discard.com", "a.discard.com"},
},
},
}
testCases = []struct {
domain string
want bool
}{
{domain: "foo.com", want: true},
{domain: "DISCARD.com", want: false},
{domain: "a.discard.com", want: false},
{domain: "b.discard.com", want: true},
}
for _, tc := range testCases {
t.Run(tc.domain, func(t *testing.T) {
got := ap.ShouldStoreDomain(tc.domain)
if got != tc.want {
t.Errorf("Got store %v for %q, want: %v", got, tc.domain, tc.want)
}
})
}
}
func TestExtractMailboxValid(t *testing.T) {
localPolicy := policy.Addressing{Config: &config.Root{MailboxNaming: config.LocalNaming}}
fullPolicy := policy.Addressing{Config: &config.Root{MailboxNaming: config.FullNaming}}
domainPolicy := policy.Addressing{Config: &config.Root{MailboxNaming: config.DomainNaming}}
testTable := []struct {
input string // Input to test
local string // Expected output when mailbox naming = local
full string // Expected output when mailbox naming = full
domain string // Expected output when mailbox naming = domain
}{
{
input: "mailbox",
local: "mailbox",
full: "mailbox",
domain: "mailbox",
},
{
input: "user123",
local: "user123",
full: "user123",
domain: "user123",
},
{
input: "MailBOX",
local: "mailbox",
full: "mailbox",
domain: "mailbox",
},
{
input: "First.Last",
local: "first.last",
full: "first.last",
domain: "first.last",
},
{
input: "user+label",
local: "user",
full: "user",
domain: "user",
},
{
input: "chars!#$%",
local: "chars!#$%",
full: "chars!#$%",
domain: "",
},
{
input: "chars&'*-",
local: "chars&'*-",
full: "chars&'*-",
domain: "",
},
{
input: "chars=/?^",
local: "chars=/?^",
full: "chars=/?^",
domain: "",
},
{
input: "chars_`.{",
local: "chars_`.{",
full: "chars_`.{",
domain: "",
},
{
input: "chars|}~",
local: "chars|}~",
full: "chars|}~",
domain: "",
},
{
input: "mailbox@domain.com",
local: "mailbox",
full: "mailbox@domain.com",
domain: "domain.com",
},
{
input: "user123@domain.com",
local: "user123",
full: "user123@domain.com",
domain: "domain.com",
},
{
input: "MailBOX@domain.com",
local: "mailbox",
full: "mailbox@domain.com",
domain: "domain.com",
},
{
input: "First.Last@domain.com",
local: "first.last",
full: "first.last@domain.com",
domain: "domain.com",
},
{
input: "user+label@domain.com",
local: "user",
full: "user@domain.com",
domain: "domain.com",
},
{
input: "chars!#$%@domain.com",
local: "chars!#$%",
full: "chars!#$%@domain.com",
domain: "domain.com",
},
{
input: "chars&'*-@domain.com",
local: "chars&'*-",
full: "chars&'*-@domain.com",
domain: "domain.com",
},
{
input: "chars=/?^@domain.com",
local: "chars=/?^",
full: "chars=/?^@domain.com",
domain: "domain.com",
},
{
input: "chars_`.{@domain.com",
local: "chars_`.{",
full: "chars_`.{@domain.com",
domain: "domain.com",
},
{
input: "chars|}~@domain.com",
local: "chars|}~",
full: "chars|}~@domain.com",
domain: "domain.com",
},
{
input: "chars|}~@example.co.uk",
local: "chars|}~",
full: "chars|}~@example.co.uk",
domain: "example.co.uk",
},
{
input: "@host:user+label@domain.com",
local: "user",
full: "user@domain.com",
domain: "domain.com",
},
{
input: "@a.com,@b.com:user+label@domain.com",
local: "user",
full: "user@domain.com",
domain: "domain.com",
},
{
input: "u@[127.0.0.1]",
local: "u",
full: "u@[127.0.0.1]",
domain: "[127.0.0.1]",
},
{
input: "u@[IPv6:2001:db8:aaaa:1::100]",
local: "u",
full: "u@[IPv6:2001:db8:aaaa:1::100]",
domain: "[IPv6:2001:db8:aaaa:1::100]",
},
}
for _, tc := range testTable {
if result, err := localPolicy.ExtractMailbox(tc.input); err != nil {
t.Errorf("Error while parsing with local naming %q: %v", tc.input, err)
} else if result != tc.local {
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.local, result)
}
if result, err := fullPolicy.ExtractMailbox(tc.input); err != nil {
t.Errorf("Error while parsing with full naming %q: %v", tc.input, err)
} else if result != tc.full {
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.full, result)
}
if result, err := domainPolicy.ExtractMailbox(tc.input); tc.domain != "" && err != nil {
t.Errorf("Error while parsing with domain naming %q: %v", tc.input, err)
} else if result != tc.domain {
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.domain, result)
}
}
}
// Test special cases with domain addressing mode.
func TestExtractDomainMailboxValid(t *testing.T) {
domainPolicy := policy.Addressing{Config: &config.Root{MailboxNaming: config.DomainNaming}}
tests := map[string]struct {
input string // Input to test
domain string // Expected output when mailbox naming = domain
}{
"ipv4": {
input: "[127.0.0.1]",
domain: "[127.0.0.1]",
},
"medium ipv6": {
input: "[IPv6:2001:db8:aaaa:1::100]",
domain: "[IPv6:2001:db8:aaaa:1::100]",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
if result, err := domainPolicy.ExtractMailbox(tc.input); tc.domain != "" && err != nil {
t.Errorf("Error while parsing with domain naming %q: %v", tc.input, err)
} else if result != tc.domain {
t.Errorf("Parsing %q, expected %q, got %q", tc.input, tc.domain, result)
}
})
}
}
func TestExtractMailboxInvalid(t *testing.T) {
localPolicy := policy.Addressing{Config: &config.Root{MailboxNaming: config.LocalNaming}}
fullPolicy := policy.Addressing{Config: &config.Root{MailboxNaming: config.FullNaming}}
domainPolicy := policy.Addressing{Config: &config.Root{MailboxNaming: config.DomainNaming}}
// Test local mailbox naming policy.
localInvalidTable := []struct {
input, msg string
}{
{"", "Empty mailbox name is not permitted"},
{"first last", "Space not permitted"},
{"first\"last", "Double quote not permitted"},
{"first\nlast", "Control chars not permitted"},
}
for _, tt := range localInvalidTable {
if _, err := localPolicy.ExtractMailbox(tt.input); err == nil {
t.Errorf("Didn't get an error while parsing in local mode %q: %v", tt.input, tt.msg)
}
}
// Test full mailbox naming policy.
fullInvalidTable := []struct {
input, msg string
}{
{"", "Empty mailbox name is not permitted"},
{"user@host@domain.com", "@ symbol not permitted"},
{"first last@domain.com", "Space not permitted"},
{"first\"last@domain.com", "Double quote not permitted"},
{"first\nlast@domain.com", "Control chars not permitted"},
}
for _, tt := range fullInvalidTable {
if _, err := fullPolicy.ExtractMailbox(tt.input); err == nil {
t.Errorf("Didn't get an error while parsing in full mode %q: %v", tt.input, tt.msg)
}
}
// Test domain mailbox naming policy.
domainInvalidTable := []struct {
input, msg string
}{
{"", "Empty mailbox name is not permitted"},
{"user@host@domain.com", "@ symbol not permitted"},
{"first.last@dom ain.com", "Space not permitted"},
{"first\"last@domain.com", "Double quote not permitted"},
{"first\nlast@domain.com", "Control chars not permitted"},
{"first.last@chars!#$%.com", "Invalid domain name"},
{"first.last@.example.com", "Domain cannot start with dot"},
{"first.last@-example.com", "Domain canont start with dash"},
{"first.last@example.com-", "Domain cannot end with dash"},
{"first.last@example..com", "Domain cannot contain double dots"},
{"first.last@example--com", "Domain cannot contain double dashes"},
{"first.last@example.-com", "Domain cannot contain concecutive symbols"},
}
for _, tt := range domainInvalidTable {
if _, err := domainPolicy.ExtractMailbox(tt.input); err == nil {
t.Errorf("Didn't get an error while parsing in domain mode %q: %v", tt.input, tt.msg)
}
}
}
func TestValidateDomain(t *testing.T) {
testTable := []struct {
input string
expect bool
msg string
}{
{"", false, "Empty domain is not valid"},
{"hostname", true, "Just a hostname is valid"},
{"github.com", true, "Two labels should be just fine"},
{"my-domain.com", true, "Hyphen is allowed mid-label"},
{"_domainkey.foo.com", true, "Underscores are allowed"},
{"bar.com.", true, "Must be able to end with a dot"},
{"ABC.6DBS.com", true, "Mixed case is OK"},
{"mail.123.com", true, "Number only label valid"},
{"123.com", true, "Number only label valid"},
{"google..com", false, "Double dot not valid"},
{".foo.com", false, "Cannot start with a dot"},
{"google\r.com", false, "Special chars not allowed"},
{"foo.-bar.com", false, "Label cannot start with hyphen"},
{"foo-.bar.com", false, "Label cannot end with hyphen"},
{strings.Repeat("a", 256), false, "Max domain length is 255"},
{strings.Repeat("a", 63) + ".com", true, "Should allow 63 char domain label"},
{strings.Repeat("a", 64) + ".com", false, "Max domain label length is 63"},
{"[0.0.0.0]", true, "Single digit octet IP addr is valid"},
{"[123.123.123.123]", true, "Multiple digit octet IP addr is valid"},
{"[IPv6:2001:0db8:aaaa:0001:0000:0000:0000:0200]", true, "Full IPv6 addr is valid"},
{"[IPv6:::1]", true, "Abbr IPv6 addr is valid"},
}
for _, tt := range testTable {
if policy.ValidateDomainPart(tt.input) != tt.expect {
t.Errorf("Expected %v for %q: %s", tt.expect, tt.input, tt.msg)
}
}
}
func TestValidateLocal(t *testing.T) {
testTable := []struct {
input string
expect bool
msg string
}{
{"", false, "Empty local is not valid"},
{"a", true, "Single letter should be fine"},
{strings.Repeat("a", 128), true, "Valid up to 128 characters"},
{strings.Repeat("a", 129), false, "Only valid up to 128 characters"},
{"FirstLast", true, "Mixed case permitted"},
{"user123", true, "Numbers permitted"},
{"a!#$%&'*+-/=?^_`{|}~", true, "Any of !#$%&'*+-/=?^_`{|}~ are permitted"},
{"first.last", true, "Embedded period is permitted"},
{"first..last", false, "Sequence of periods is not allowed"},
{".user", false, "Cannot lead with a period"},
{"user.", false, "Cannot end with a period"},
// {"james@mail", false, "Unquoted @ not permitted"},
{"first last", false, "Unquoted space not permitted"},
{"tricky\\. ", false, "Unquoted space not permitted"},
{"no,commas", false, "Unquoted comma not allowed"},
{"t[es]t", false, "Unquoted square brackets not allowed"},
// {"james\\", false, "Cannot end with backslash quote"},
{"james\\@mail", true, "Quoted @ permitted"},
{"quoted\\ space", true, "Quoted space permitted"},
{"no\\,commas", true, "Quoted comma is OK"},
{"t\\[es\\]t", true, "Quoted brackets are OK"},
{"user\\name", true, "Should be able to quote a-z"},
{"USER\\NAME", true, "Should be able to quote A-Z"},
{"user\\1", true, "Should be able to quote a digit"},
{"one\\$\\|", true, "Should be able to quote plain specials"},
{"return\\\r", true, "Should be able to quote ASCII control chars"},
{"high\\\x80", false, "Should not accept > 7-bit quoted chars"},
{"quote\\\"", true, "Quoted double quote is permitted"},
{"\"james\"", true, "Quoted a-z is permitted"},
{"\"first last\"", true, "Quoted space is permitted"},
{"\"quoted@sign\"", true, "Quoted @ is allowed"},
{"\"qp\\\"quote\"", true, "Quoted quote within quoted string is OK"},
{"\"unterminated", false, "Quoted string must be terminated"},
{"\"unterminated\\\"", false, "Quoted string must be terminated"},
{"embed\"quote\"string", false, "Embedded quoted string is illegal"},
{"user+mailbox", true, "RFC3696 test case should be valid"},
{"customer/department=shipping", true, "RFC3696 test case should be valid"},
{"$A12345", true, "RFC3696 test case should be valid"},
{"!def!xyz%abc", true, "RFC3696 test case should be valid"},
{"_somename", true, "RFC3696 test case should be valid"},
{"@host:mailbox", true, "Forward-path routes are valid"},
{"@a.com,@b.com:mailbox", true, "Multi-hop forward-path routes are valid"},
{"@a.com,mailbox", false, "Unterminated forward-path routes are invalid"},
}
for _, tt := range testTable {
_, _, err := policy.ParseEmailAddress(tt.input + "@domain.com")
if (err != nil) == tt.expect {
if err != nil {
t.Logf("Got error: %s", err)
}
t.Errorf("Expected %v for %q: %s", tt.expect, tt.input, tt.msg)
}
}
}
// TestRecipientAddress verifies the Recipient.Address values returned by Addressing.NewRecipient.
// This function parses a RCPT TO path, not a To header. See rfc5321#section-4.1.2
func TestRecipientAddress(t *testing.T) {
localPolicy := policy.Addressing{Config: &config.Root{MailboxNaming: config.LocalNaming}}
tests := map[string]string{
"common": "user@example.com",
"with label": "user+mailbox@example.com",
"special chars": "a!#$%&'*+-/=?^_`{|}~@example.com",
"ipv4": "user@[127.0.0.1]",
"ipv6": "user@[IPv6:::1]",
"route host": "@host:user@example.com",
"route domain": "@route.com:user@example.com",
"multi-hop route": "@first.com,@second.com:user@example.com",
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
r, err := localPolicy.NewRecipient(tc)
if err != nil {
t.Fatalf("Parse of %q failed: %v", tc, err)
}
if got, want := r.Address.Address, tc; got != want {
t.Errorf("Got Address: %q, want: %q", got, want)
}
})
}
}

Some files were not shown because too many files have changed in this diff Show More