mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3062b70ea0 | ||
|
|
01d51302c4 | ||
|
|
c750dcff81 | ||
|
|
de75b778c0 | ||
|
|
b28e1d86d8 | ||
|
|
f4fadd7e44 | ||
|
|
28b40eb94d | ||
|
|
0f67e51e56 | ||
|
|
a457b65603 | ||
|
|
890d8e0202 | ||
|
|
9f6dee640e | ||
|
|
095796c8a1 | ||
|
|
db358fea8c | ||
|
|
86554a63b8 | ||
|
|
1efe2ba48f | ||
|
|
f597687aa3 | ||
|
|
6368e3a83b | ||
|
|
ef17ad9074 | ||
|
|
7908e41212 | ||
|
|
a9b174bcb6 | ||
|
|
dc0b9b325e | ||
|
|
0a967f0f21 | ||
|
|
304a2260e8 | ||
|
|
9fc9a333a6 | ||
|
|
3e8b914f89 | ||
|
|
5e94f7b750 | ||
|
|
64e75face8 | ||
|
|
be4675b374 | ||
|
|
6722811425 | ||
|
|
56cff6296a | ||
|
|
a1e35009e0 | ||
|
|
cc0428ab9b | ||
|
|
68e35b5eca | ||
|
|
5ef3adc88e | ||
|
|
1742bf9a34 | ||
|
|
c2779ff054 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -25,10 +25,13 @@ _testmain.go
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# our binary
|
||||
# our binaries
|
||||
/inbucket
|
||||
/inbucket.exe
|
||||
/dist/**
|
||||
/target/**
|
||||
/cmd/client/client
|
||||
/cmd/client/client.exe
|
||||
|
||||
# local goxc config
|
||||
.goxc.local.json
|
||||
|
||||
60
.goreleaser.yml
Normal file
60
.goreleaser.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
project_name: inbucket
|
||||
release:
|
||||
github:
|
||||
owner: jhillyerd
|
||||
name: inbucket
|
||||
name_template: '{{.Tag}}'
|
||||
brew:
|
||||
commit_author:
|
||||
name: goreleaserbot
|
||||
email: goreleaser@carlosbecker.com
|
||||
install: bin.install ""
|
||||
builds:
|
||||
- binary: inbucket
|
||||
goos:
|
||||
- darwin
|
||||
- freebsd
|
||||
- linux
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
goarm:
|
||||
- "6"
|
||||
main: .
|
||||
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
|
||||
- binary: client
|
||||
goos:
|
||||
- darwin
|
||||
- freebsd
|
||||
- linux
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
goarm:
|
||||
- "6"
|
||||
main: ./cmd/client
|
||||
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
|
||||
archive:
|
||||
format: tar.gz
|
||||
wrap_in_directory: true
|
||||
name_template: '{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{
|
||||
.Arm }}{{ end }}'
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- LICENSE*
|
||||
- README*
|
||||
- CHANGELOG*
|
||||
- inbucket.bat
|
||||
- etc/**/*
|
||||
- themes/**/*
|
||||
fpm:
|
||||
bindir: /usr/local/bin
|
||||
snapshot:
|
||||
name_template: SNAPSHOT-{{ .Commit }}
|
||||
checksum:
|
||||
name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt'
|
||||
dist: dist
|
||||
sign:
|
||||
artifacts: none
|
||||
@@ -7,7 +7,6 @@
|
||||
"Os": "darwin freebsd linux windows",
|
||||
"ResourcesInclude": "README*,LICENSE*,CHANGELOG*,inbucket.bat,etc,themes",
|
||||
"PackageVersion": "1.2.0",
|
||||
"PrereleaseInfo": "rc1",
|
||||
"ConfigVersion": "0.9",
|
||||
"BuildSettings": {
|
||||
"LdFlagsXVars": {
|
||||
|
||||
16
.travis.yml
16
.travis.yml
@@ -1,9 +1,21 @@
|
||||
language: go
|
||||
sudo: false
|
||||
|
||||
env:
|
||||
- DEPLOY_WITH_MAJOR="1.9"
|
||||
|
||||
before_script:
|
||||
- go vet ./...
|
||||
|
||||
go:
|
||||
- 1.7.5
|
||||
- master
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
|
||||
script: go test -race -v ./...
|
||||
|
||||
deploy:
|
||||
provider: script
|
||||
script: etc/travis-deploy.sh
|
||||
on:
|
||||
tags: true
|
||||
branch: master
|
||||
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -4,6 +4,29 @@ Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
[1.2.0] - 2017-12-27
|
||||
--------------------
|
||||
|
||||
### Changed
|
||||
|
||||
- No significant code changes from rc2
|
||||
|
||||
[1.2.0-rc2] - 2017-12-15
|
||||
------------------------
|
||||
|
||||
### Added
|
||||
- `rest/client` types `MessageHeader` and `Message` with convenience methods;
|
||||
provides a more natural API
|
||||
- Powerful command line REST
|
||||
[client](https://github.com/jhillyerd/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
|
||||
|
||||
[1.2.0-rc1] - 2017-01-29
|
||||
------------------------
|
||||
|
||||
@@ -76,6 +99,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
specific message.
|
||||
|
||||
[Unreleased]: https://github.com/jhillyerd/inbucket/compare/master...develop
|
||||
[1.2.0]: https://github.com/jhillyerd/inbucket/compare/1.2.0-rc2...1.2.0
|
||||
[1.2.0-rc2]: https://github.com/jhillyerd/inbucket/compare/1.2.0-rc1...1.2.0-rc2
|
||||
[1.2.0-rc1]: https://github.com/jhillyerd/inbucket/compare/1.1.0...1.2.0-rc1
|
||||
[1.1.0]: https://github.com/jhillyerd/inbucket/compare/1.1.0-rc2...1.1.0
|
||||
[1.1.0-rc2]: https://github.com/jhillyerd/inbucket/compare/1.1.0-rc1...1.1.0-rc2
|
||||
@@ -91,7 +116,7 @@ Release Checklist
|
||||
- Ensure *Unreleased* section is up to date
|
||||
- Rename *Unreleased* section to release name and date.
|
||||
- Add new GitHub `/compare` link
|
||||
3. Update goxc version info: `goxc -wc -pv=1.x.0 -pr=snapshot`
|
||||
3. Update goxc version info: `goxc -wc -pv=1.x.0 -pr=rc1`
|
||||
4. Run: `goxc interpolate-source` to update VERSION var
|
||||
5. Run tests
|
||||
6. Test cross-compile: `goxc`
|
||||
|
||||
@@ -4,6 +4,8 @@ How to Contribute
|
||||
Inbucket encourages third-party patches. It's valuable to know how other
|
||||
developers are using the product.
|
||||
|
||||
**tl;dr:** File pull requests against the `develop` branch, not `master`!
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Docker build file for Inbucket, see https://www.docker.io/
|
||||
# Inbucket website: http://www.inbucket.org/
|
||||
|
||||
FROM golang:1.7-alpine
|
||||
FROM golang:1.9-alpine
|
||||
MAINTAINER James Hillyerd, @jameshillyerd
|
||||
|
||||
# Configuration (WORKDIR doesn't support env vars)
|
||||
|
||||
54
cmd/client/list.go
Normal file
54
cmd/client/list.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/subcommands"
|
||||
"github.com/jhillyerd/inbucket/rest/client"
|
||||
)
|
||||
|
||||
type listCmd struct {
|
||||
mailbox string
|
||||
}
|
||||
|
||||
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(
|
||||
_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
mailbox := f.Arg(0)
|
||||
if mailbox == "" {
|
||||
return usage("mailbox required")
|
||||
}
|
||||
// Setup rest client
|
||||
c, err := client.New(baseURL())
|
||||
if err != nil {
|
||||
return fatal("Couldn't build client", err)
|
||||
}
|
||||
// Get list
|
||||
headers, err := c.ListMailbox(mailbox)
|
||||
if err != nil {
|
||||
return fatal("REST call failed", err)
|
||||
}
|
||||
for _, h := range headers {
|
||||
fmt.Println(h.ID)
|
||||
}
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
79
cmd/client/main.go
Normal file
79
cmd/client/main.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Package main implements a command line client for the Inbucket REST API
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"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 = ®exFlag{}
|
||||
|
||||
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 fmt.Sprintf("http://%s:%v", *host, *port)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
164
cmd/client/match.go
Normal file
164
cmd/client/match.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/google/subcommands"
|
||||
"github.com/jhillyerd/inbucket/rest/client"
|
||||
)
|
||||
|
||||
type matchCmd struct {
|
||||
mailbox string
|
||||
output string
|
||||
outFunc func(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(
|
||||
_ 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.ListMailbox(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(matches)
|
||||
if err != nil {
|
||||
return fatal("Error", err)
|
||||
}
|
||||
if m.delete {
|
||||
// Delete matches
|
||||
for _, h := range matches {
|
||||
err = h.Delete()
|
||||
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(headers []*client.MessageHeader) error {
|
||||
for _, h := range headers {
|
||||
fmt.Println(h.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputJSON(headers []*client.MessageHeader) error {
|
||||
jsonEncoder := json.NewEncoder(os.Stdout)
|
||||
jsonEncoder.SetEscapeHTML(false)
|
||||
jsonEncoder.SetIndent("", " ")
|
||||
return jsonEncoder.Encode(headers)
|
||||
}
|
||||
82
cmd/client/mbox.go
Normal file
82
cmd/client/mbox.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/google/subcommands"
|
||||
"github.com/jhillyerd/inbucket/rest/client"
|
||||
)
|
||||
|
||||
type mboxCmd struct {
|
||||
mailbox string
|
||||
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(
|
||||
_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
mailbox := f.Arg(0)
|
||||
if mailbox == "" {
|
||||
return usage("mailbox required")
|
||||
}
|
||||
// Setup REST client
|
||||
c, err := client.New(baseURL())
|
||||
if err != nil {
|
||||
return fatal("Couldn't build client", err)
|
||||
}
|
||||
// Get list
|
||||
headers, err := c.ListMailbox(mailbox)
|
||||
if err != nil {
|
||||
return fatal("List REST call failed", err)
|
||||
}
|
||||
err = outputMbox(headers)
|
||||
if err != nil {
|
||||
return fatal("Error", err)
|
||||
}
|
||||
if m.delete {
|
||||
// Delete matches
|
||||
for _, h := range headers {
|
||||
err = h.Delete()
|
||||
if err != nil {
|
||||
return fatal("Delete REST call failed", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
// outputMbox renders messages in mbox format
|
||||
// also used by match subcommand
|
||||
func outputMbox(headers []*client.MessageHeader) error {
|
||||
for _, h := range headers {
|
||||
source, err := h.GetSource()
|
||||
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 >
|
||||
source.WriteTo(os.Stdout)
|
||||
fmt.Println()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -15,11 +15,14 @@ apk add --no-cache --virtual .build-deps git
|
||||
|
||||
# Setup
|
||||
export GOBIN="$bindir"
|
||||
builddate="$(date -Iseconds)"
|
||||
cd "$srcdir"
|
||||
go clean
|
||||
# Fetch tags for describe
|
||||
git fetch -t
|
||||
builddate="$(date -Iseconds)"
|
||||
buildver="$(git describe --tags --always)"
|
||||
|
||||
# Build
|
||||
go clean
|
||||
echo "### Fetching Dependencies"
|
||||
go get -t -v ./...
|
||||
|
||||
@@ -27,7 +30,7 @@ echo "### Testing Inbucket"
|
||||
go test ./...
|
||||
|
||||
echo "### Building Inbucket"
|
||||
go build -o inbucket -ldflags "-X 'main.BUILDDATE=$builddate'" -v .
|
||||
go build -o inbucket -ldflags "-X 'main.version=$buildver' -X 'main.date=$builddate'" -v .
|
||||
|
||||
echo "### Installing Inbucket"
|
||||
set -x
|
||||
|
||||
10
etc/travis-deploy.sh
Executable file
10
etc/travis-deploy.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
# travis-deploy.sh
|
||||
# description: Trigger goreleaser deployment in correct build scenarios
|
||||
|
||||
set -eo pipefail
|
||||
set -x
|
||||
|
||||
if [[ "$TRAVIS_GO_VERSION" == "$DEPLOY_WITH_MAJOR."* ]]; then
|
||||
curl -sL https://git.io/goreleaser | bash
|
||||
fi
|
||||
12
inbucket.go
12
inbucket.go
@@ -23,11 +23,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// VERSION contains the build version number, populated during linking by goxc
|
||||
VERSION = "1.2.0-rc1"
|
||||
// version contains the build version number, populated during linking
|
||||
version = "1.2.0"
|
||||
|
||||
// BUILDDATE contains the build date, populated during linking by goxc
|
||||
BUILDDATE = "undefined"
|
||||
// date contains the build date, populated during linking
|
||||
date = "undefined"
|
||||
|
||||
// Command line flags
|
||||
help = flag.Bool("help", false, "Displays this help")
|
||||
@@ -61,8 +61,8 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.Version = VERSION
|
||||
config.BuildDate = BUILDDATE
|
||||
config.Version = version
|
||||
config.BuildDate = date
|
||||
|
||||
flag.Parse()
|
||||
if *help {
|
||||
|
||||
@@ -63,13 +63,15 @@ func New(ctx context.Context, historyLen int) *Hub {
|
||||
// history buffer and then relayed to all registered listeners.
|
||||
func (hub *Hub) Dispatch(msg Message) {
|
||||
hub.opChan <- func(h *Hub) {
|
||||
// Add to history buffer
|
||||
h.history.Value = msg
|
||||
h.history = h.history.Next()
|
||||
// Deliver message 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)
|
||||
if h.history != nil {
|
||||
// Add to history buffer
|
||||
h.history.Value = msg
|
||||
h.history = h.history.Next()
|
||||
// Deliver message 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,17 @@ func TestHubNew(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHubZeroLen(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
hub := New(ctx, 0)
|
||||
m := Message{}
|
||||
for i := 0; i < 100; i++ {
|
||||
hub.Dispatch(m)
|
||||
}
|
||||
// Just making sure Hub doesn't panic
|
||||
}
|
||||
|
||||
func TestHubZeroListeners(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package client provides a basic REST client for Inbucket
|
||||
package client
|
||||
|
||||
import (
|
||||
@@ -10,19 +11,19 @@ import (
|
||||
"github.com/jhillyerd/inbucket/rest/model"
|
||||
)
|
||||
|
||||
// ClientV1 accesses the Inbucket REST API v1
|
||||
type ClientV1 struct {
|
||||
// Client accesses the Inbucket REST API v1
|
||||
type Client struct {
|
||||
restClient
|
||||
}
|
||||
|
||||
// NewV1 creates a new v1 REST API client given the base URL of an Inbucket server, ex:
|
||||
// New creates a new v1 REST API client given the base URL of an Inbucket server, ex:
|
||||
// "http://localhost:9000"
|
||||
func NewV1(baseURL string) (*ClientV1, error) {
|
||||
func New(baseURL string) (*Client, error) {
|
||||
parsedURL, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &ClientV1{
|
||||
c := &Client{
|
||||
restClient{
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
@@ -34,21 +35,31 @@ func NewV1(baseURL string) (*ClientV1, error) {
|
||||
}
|
||||
|
||||
// ListMailbox returns a list of messages for the requested mailbox
|
||||
func (c *ClientV1) ListMailbox(name string) (headers []*model.JSONMessageHeaderV1, err error) {
|
||||
func (c *Client) ListMailbox(name string) (headers []*MessageHeader, err error) {
|
||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name)
|
||||
err = c.doJSON("GET", uri, &headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, h := range headers {
|
||||
h.client = c
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetMessage returns the message details given a mailbox name and message ID.
|
||||
func (c *ClientV1) GetMessage(name, id string) (message *model.JSONMessageV1, err error) {
|
||||
func (c *Client) GetMessage(name, id string) (message *Message, err error) {
|
||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id
|
||||
err = c.doJSON("GET", uri, &message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
message.client = c
|
||||
return
|
||||
}
|
||||
|
||||
// GetMessageSource returns the message source given a mailbox name and message ID.
|
||||
func (c *ClientV1) GetMessageSource(name, id string) (*bytes.Buffer, error) {
|
||||
func (c *Client) GetMessageSource(name, id string) (*bytes.Buffer, error) {
|
||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id + "/source"
|
||||
resp, err := c.do("GET", uri)
|
||||
if err != nil {
|
||||
@@ -68,7 +79,7 @@ func (c *ClientV1) GetMessageSource(name, id string) (*bytes.Buffer, error) {
|
||||
}
|
||||
|
||||
// DeleteMessage deletes a single message given the mailbox name and message ID.
|
||||
func (c *ClientV1) DeleteMessage(name, id string) error {
|
||||
func (c *Client) DeleteMessage(name, id string) error {
|
||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id
|
||||
resp, err := c.do("DELETE", uri)
|
||||
if err != nil {
|
||||
@@ -82,7 +93,7 @@ func (c *ClientV1) DeleteMessage(name, id string) error {
|
||||
}
|
||||
|
||||
// PurgeMailbox deletes all messages in the given mailbox
|
||||
func (c *ClientV1) PurgeMailbox(name string) error {
|
||||
func (c *Client) PurgeMailbox(name string) error {
|
||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name)
|
||||
resp, err := c.do("DELETE", uri)
|
||||
if err != nil {
|
||||
@@ -94,3 +105,40 @@ func (c *ClientV1) PurgeMailbox(name string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MessageHeader represents an Inbucket message sans content
|
||||
type MessageHeader struct {
|
||||
*model.JSONMessageHeaderV1
|
||||
client *Client
|
||||
}
|
||||
|
||||
// GetMessage returns this message with content
|
||||
func (h *MessageHeader) GetMessage() (message *Message, err error) {
|
||||
return h.client.GetMessage(h.Mailbox, h.ID)
|
||||
}
|
||||
|
||||
// GetSource returns the source for this message
|
||||
func (h *MessageHeader) GetSource() (*bytes.Buffer, error) {
|
||||
return h.client.GetMessageSource(h.Mailbox, h.ID)
|
||||
}
|
||||
|
||||
// Delete deletes this message from the mailbox
|
||||
func (h *MessageHeader) Delete() error {
|
||||
return h.client.DeleteMessage(h.Mailbox, h.ID)
|
||||
}
|
||||
|
||||
// Message represents an Inbucket message including content
|
||||
type Message struct {
|
||||
*model.JSONMessageV1
|
||||
client *Client
|
||||
}
|
||||
|
||||
// GetSource returns the source for this message
|
||||
func (m *Message) GetSource() (*bytes.Buffer, error) {
|
||||
return m.client.GetMessageSource(m.Mailbox, m.ID)
|
||||
}
|
||||
|
||||
// Delete deletes this message from the mailbox
|
||||
func (m *Message) Delete() error {
|
||||
return m.client.DeleteMessage(m.Mailbox, m.ID)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import "testing"
|
||||
func TestClientV1ListMailbox(t *testing.T) {
|
||||
var want, got string
|
||||
|
||||
c, err := NewV1(baseURLStr)
|
||||
c, err := New(baseURLStr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ func TestClientV1ListMailbox(t *testing.T) {
|
||||
c.client = mth
|
||||
|
||||
// Method under test
|
||||
c.ListMailbox("testbox")
|
||||
_, _ = c.ListMailbox("testbox")
|
||||
|
||||
want = "GET"
|
||||
got = mth.req.Method
|
||||
@@ -31,7 +31,7 @@ func TestClientV1ListMailbox(t *testing.T) {
|
||||
func TestClientV1GetMessage(t *testing.T) {
|
||||
var want, got string
|
||||
|
||||
c, err := NewV1(baseURLStr)
|
||||
c, err := New(baseURLStr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func TestClientV1GetMessage(t *testing.T) {
|
||||
c.client = mth
|
||||
|
||||
// Method under test
|
||||
c.GetMessage("testbox", "20170107T224128-0000")
|
||||
_, _ = c.GetMessage("testbox", "20170107T224128-0000")
|
||||
|
||||
want = "GET"
|
||||
got = mth.req.Method
|
||||
@@ -57,13 +57,12 @@ func TestClientV1GetMessage(t *testing.T) {
|
||||
func TestClientV1GetMessageSource(t *testing.T) {
|
||||
var want, got string
|
||||
|
||||
c, err := NewV1(baseURLStr)
|
||||
c, err := New(baseURLStr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mth := &mockHTTPClient{
|
||||
statusCode: 200,
|
||||
body: "message source",
|
||||
body: "message source",
|
||||
}
|
||||
c.client = mth
|
||||
|
||||
@@ -95,7 +94,7 @@ func TestClientV1GetMessageSource(t *testing.T) {
|
||||
func TestClientV1DeleteMessage(t *testing.T) {
|
||||
var want, got string
|
||||
|
||||
c, err := NewV1(baseURLStr)
|
||||
c, err := New(baseURLStr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -103,7 +102,10 @@ func TestClientV1DeleteMessage(t *testing.T) {
|
||||
c.client = mth
|
||||
|
||||
// Method under test
|
||||
c.DeleteMessage("testbox", "20170107T224128-0000")
|
||||
err = c.DeleteMessage("testbox", "20170107T224128-0000")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want = "DELETE"
|
||||
got = mth.req.Method
|
||||
@@ -121,7 +123,7 @@ func TestClientV1DeleteMessage(t *testing.T) {
|
||||
func TestClientV1PurgeMailbox(t *testing.T) {
|
||||
var want, got string
|
||||
|
||||
c, err := NewV1(baseURLStr)
|
||||
c, err := New(baseURLStr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -129,7 +131,10 @@ func TestClientV1PurgeMailbox(t *testing.T) {
|
||||
c.client = mth
|
||||
|
||||
// Method under test
|
||||
c.PurgeMailbox("testbox")
|
||||
err = c.PurgeMailbox("testbox")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want = "DELETE"
|
||||
got = mth.req.Method
|
||||
@@ -143,3 +148,176 @@ func TestClientV1PurgeMailbox(t *testing.T) {
|
||||
t.Errorf("req.URL == %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientV1MessageHeader(t *testing.T) {
|
||||
var want, got string
|
||||
response := `[
|
||||
{
|
||||
"mailbox":"mailbox1",
|
||||
"id":"id1",
|
||||
"from":"from1",
|
||||
"subject":"subject1",
|
||||
"date":"2017-01-01T00:00:00.000-07:00",
|
||||
"size":100
|
||||
}
|
||||
]`
|
||||
|
||||
c, err := New(baseURLStr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mth := &mockHTTPClient{body: response}
|
||||
c.client = mth
|
||||
|
||||
// Method under test
|
||||
headers, err := c.ListMailbox("testbox")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want = "GET"
|
||||
got = mth.req.Method
|
||||
if got != want {
|
||||
t.Errorf("req.Method == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
want = baseURLStr + "/api/v1/mailbox/testbox"
|
||||
got = mth.req.URL.String()
|
||||
if got != want {
|
||||
t.Errorf("req.URL == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if len(headers) != 1 {
|
||||
t.Fatalf("len(headers) == %v, want 1", len(headers))
|
||||
}
|
||||
header := headers[0]
|
||||
|
||||
want = "mailbox1"
|
||||
got = header.Mailbox
|
||||
if got != want {
|
||||
t.Errorf("Mailbox == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
want = "id1"
|
||||
got = header.ID
|
||||
if got != want {
|
||||
t.Errorf("ID == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
want = "from1"
|
||||
got = header.From
|
||||
if got != want {
|
||||
t.Errorf("From == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
want = "subject1"
|
||||
got = header.Subject
|
||||
if got != want {
|
||||
t.Errorf("Subject == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// Test MessageHeader.Delete()
|
||||
mth.body = ""
|
||||
err = header.Delete()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want = "DELETE"
|
||||
got = mth.req.Method
|
||||
if got != want {
|
||||
t.Errorf("req.Method == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
want = baseURLStr + "/api/v1/mailbox/mailbox1/id1"
|
||||
got = mth.req.URL.String()
|
||||
if got != want {
|
||||
t.Errorf("req.URL == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// Test MessageHeader.GetSource()
|
||||
mth.body = "source1"
|
||||
_, err = header.GetSource()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want = "GET"
|
||||
got = mth.req.Method
|
||||
if got != want {
|
||||
t.Errorf("req.Method == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
want = baseURLStr + "/api/v1/mailbox/mailbox1/id1/source"
|
||||
got = mth.req.URL.String()
|
||||
if got != want {
|
||||
t.Errorf("req.URL == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// Test MessageHeader.GetMessage()
|
||||
mth.body = `{
|
||||
"mailbox":"mailbox1",
|
||||
"id":"id1",
|
||||
"from":"from1",
|
||||
"subject":"subject1",
|
||||
"date":"2017-01-01T00:00:00.000-07:00",
|
||||
"size":100
|
||||
}`
|
||||
message, err := header.GetMessage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if message == nil {
|
||||
t.Fatalf("message was nil, wanted a value")
|
||||
}
|
||||
|
||||
want = "GET"
|
||||
got = mth.req.Method
|
||||
if got != want {
|
||||
t.Errorf("req.Method == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
want = baseURLStr + "/api/v1/mailbox/mailbox1/id1"
|
||||
got = mth.req.URL.String()
|
||||
if got != want {
|
||||
t.Errorf("req.URL == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// Test Message.Delete()
|
||||
mth.body = ""
|
||||
err = message.Delete()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want = "DELETE"
|
||||
got = mth.req.Method
|
||||
if got != want {
|
||||
t.Errorf("req.Method == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
want = baseURLStr + "/api/v1/mailbox/mailbox1/id1"
|
||||
got = mth.req.URL.String()
|
||||
if got != want {
|
||||
t.Errorf("req.URL == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// Test MessageHeader.GetSource()
|
||||
mth.body = "source1"
|
||||
_, err = message.GetSource()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want = "GET"
|
||||
got = mth.req.Method
|
||||
if got != want {
|
||||
t.Errorf("req.Method == %q, want %q", got, want)
|
||||
}
|
||||
|
||||
want = baseURLStr + "/api/v1/mailbox/mailbox1/id1/source"
|
||||
got = mth.req.URL.String()
|
||||
if got != want {
|
||||
t.Errorf("req.URL == %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +50,9 @@ func (c *restClient) doJSON(method string, uri string, v interface{}) error {
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
if v == nil {
|
||||
return nil
|
||||
} else {
|
||||
// Decode response body
|
||||
return json.NewDecoder(resp.Body).Decode(v)
|
||||
}
|
||||
// Decode response body
|
||||
return json.NewDecoder(resp.Body).Decode(v)
|
||||
}
|
||||
|
||||
return fmt.Errorf("Unexpected HTTP response status %v: %s", resp.StatusCode, resp.Status)
|
||||
|
||||
@@ -28,6 +28,9 @@ type mockHTTPClient struct {
|
||||
|
||||
func (m *mockHTTPClient) Do(req *http.Request) (resp *http.Response, err error) {
|
||||
m.req = req
|
||||
if m.statusCode == 0 {
|
||||
m.statusCode = 200
|
||||
}
|
||||
resp = &http.Response{
|
||||
StatusCode: m.statusCode,
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(m.body)),
|
||||
@@ -64,13 +67,15 @@ func TestDoJSON(t *testing.T) {
|
||||
var want, got string
|
||||
|
||||
mth := &mockHTTPClient{
|
||||
statusCode: 200,
|
||||
body: `{"foo": "bar"}`,
|
||||
body: `{"foo": "bar"}`,
|
||||
}
|
||||
c := &restClient{mth, baseURL}
|
||||
|
||||
var v map[string]interface{}
|
||||
c.doJSON("GET", "/doget", &v)
|
||||
err := c.doJSON("GET", "/doget", &v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want = "GET"
|
||||
got = mth.req.Method
|
||||
@@ -98,7 +103,7 @@ func TestDoJSON(t *testing.T) {
|
||||
func TestDoJSONNilV(t *testing.T) {
|
||||
var want, got string
|
||||
|
||||
mth := &mockHTTPClient{statusCode: 200}
|
||||
mth := &mockHTTPClient{}
|
||||
c := &restClient{mth, baseURL}
|
||||
|
||||
err := c.doJSON("GET", "/doget", nil)
|
||||
|
||||
@@ -181,9 +181,13 @@ func (mb *FileMailbox) GetMessage(id string) (Message, error) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range mb.messages {
|
||||
if m.Fid == id {
|
||||
return m, nil
|
||||
if id == "latest" && len(mb.messages) != 0 {
|
||||
return mb.messages[len(mb.messages)-1], nil
|
||||
} else {
|
||||
for _, m := range mb.messages {
|
||||
if m.Fid == id {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,22 +258,22 @@ func (mb *FileMailbox) writeIndex() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
log.Errorf("Failed to close %q: %v", mb.indexPath, err)
|
||||
}
|
||||
}()
|
||||
writer := bufio.NewWriter(file)
|
||||
|
||||
// Write each message and then flush
|
||||
enc := gob.NewEncoder(writer)
|
||||
for _, m := range mb.messages {
|
||||
err = enc.Encode(m)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := writer.Flush(); err != nil {
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
log.Errorf("Failed to close %q: %v", mb.indexPath, err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -458,6 +458,55 @@ func TestFSNoMessageCap(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test Get the latest message
|
||||
func TestGetLatestMessage(t *testing.T) {
|
||||
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
// james hashes to 474ba67bdb289c6263b36dfd8a7bed6c85b04943
|
||||
mbName := "james"
|
||||
|
||||
// Test empty mailbox
|
||||
mb, err := ds.MailboxFor(mbName)
|
||||
assert.Nil(t, err)
|
||||
msg, err := mb.GetMessage("latest")
|
||||
assert.Error(t, err)
|
||||
fmt.Println(msg)
|
||||
|
||||
// Deliver test message
|
||||
deliverMessage(ds, mbName, "test", time.Now())
|
||||
|
||||
// Deliver test message 2
|
||||
id2, _ := deliverMessage(ds, mbName, "test 2", time.Now())
|
||||
|
||||
// Test get the latest message
|
||||
mb, err = ds.MailboxFor(mbName)
|
||||
assert.Nil(t, err)
|
||||
msg, err = mb.GetMessage("latest")
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, msg.ID() == id2, "Expected %q to be equal to %q", msg.ID(), id2)
|
||||
|
||||
// Deliver test message 3
|
||||
id3, _ := deliverMessage(ds, mbName, "test 3", time.Now())
|
||||
|
||||
mb, err = ds.MailboxFor(mbName)
|
||||
assert.Nil(t, err)
|
||||
msg, err = mb.GetMessage("latest")
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, msg.ID() == id3, "Expected %q to be equal to %q", msg.ID(), id3)
|
||||
|
||||
// Test wrong id
|
||||
msg, err = mb.GetMessage("wrongid")
|
||||
assert.Error(t, err)
|
||||
|
||||
if t.Failed() {
|
||||
// Wait for handler to finish logging
|
||||
time.Sleep(2 * time.Second)
|
||||
// Dump buffered log data if there was a failure
|
||||
_, _ = io.Copy(os.Stderr, logbuf)
|
||||
}
|
||||
}
|
||||
|
||||
// setupDataStore creates a new FileDataStore in a temporary directory
|
||||
func setupDataStore(cfg config.DataStoreConfig) (*FileDataStore, *bytes.Buffer) {
|
||||
path, err := ioutil.TempDir("", "inbucket")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
# run-tests.sh
|
||||
# description: Generate test emails for Inbucket
|
||||
|
||||
|
||||
Reference in New Issue
Block a user