1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2026-02-08 03:16:00 +00:00

Merge branch 'feature/pkg' into develop, closes #79

This commit is contained in:
James Hillyerd
2018-03-10 13:02:44 -08:00
60 changed files with 263 additions and 246 deletions

4
.gitignore vendored
View File

@@ -26,8 +26,12 @@ _testmain.go
*.swo *.swo
# our binaries # our binaries
/client
/client.exe
/inbucket /inbucket
/inbucket.exe /inbucket.exe
/dist/** /dist/**
/cmd/client/client /cmd/client/client
/cmd/client/client.exe /cmd/client/client.exe
/cmd/inbucket/inbucket
/cmd/inbucket/inbucket.exe

View File

@@ -2,13 +2,12 @@ language: go
sudo: false sudo: false
env: env:
- DEPLOY_WITH_MAJOR="1.9" - DEPLOY_WITH_MAJOR="1.10"
before_script: before_script:
- go get github.com/golang/lint/golint - go get github.com/golang/lint/golint
go: go:
- 1.9.x
- "1.10" - "1.10"
deploy: deploy:

View File

@@ -1,7 +1,7 @@
# Docker build file for Inbucket, see https://www.docker.io/ # Docker build file for Inbucket, see https://www.docker.io/
# Inbucket website: http://www.inbucket.org/ # Inbucket website: http://www.inbucket.org/
FROM golang:1.9-alpine FROM golang:1.10-alpine
MAINTAINER James Hillyerd, @jameshillyerd MAINTAINER James Hillyerd, @jameshillyerd
# Configuration (WORKDIR doesn't support env vars) # Configuration (WORKDIR doesn't support env vars)

View File

@@ -1,26 +1,27 @@
PKG := inbucket SHELL = /bin/sh
SHELL := /bin/sh
SRC := $(shell find . -type f -name '*.go' -not -path "./vendor/*") SRC := $(shell find . -type f -name '*.go' -not -path "./vendor/*")
PKGS := $$(go list ./... | grep -v /vendor/) PKGS := $(shell go list ./... | grep -v /vendor/)
.PHONY: all build clean fmt install lint simplify test .PHONY: all build clean fmt lint simplify test
all: test lint build commands = client inbucket
all: clean test lint build
$(commands): %: cmd/%
go build ./$<
clean: clean:
go clean go clean $(PKGS)
rm -f $(commands)
deps: deps:
go get -t ./... go get -t ./...
build: clean deps build: deps $(commands)
go build
install: build test: deps
go install
test: clean deps
go test -race ./... go test -race ./...
fmt: fmt:
@@ -31,5 +32,5 @@ simplify:
lint: lint:
@test -z "$(shell gofmt -l . | tee /dev/stderr)" || echo "[WARN] Fix formatting issues with 'make fmt'" @test -z "$(shell gofmt -l . | tee /dev/stderr)" || echo "[WARN] Fix formatting issues with 'make fmt'"
@golint -set_exit_status $${PKGS} @golint -set_exit_status $(PKGS)
@go vet $${PKGS} @go vet $(PKGS)

View File

@@ -21,6 +21,9 @@ to contribute code to the project check out [CONTRIBUTING.md].
## Homebrew Tap ## Homebrew Tap
(currently broken, being tracked in [issue
#68](https://github.com/jhillyerd/inbucket/issues/68))
Inbucket has an OS X [Homebrew] tap available as [jhillyerd/inbucket][Homebrew Tap], Inbucket has an OS X [Homebrew] tap available as [jhillyerd/inbucket][Homebrew Tap],
see the `README.md` there for installation instructions. see the `README.md` there for installation instructions.
@@ -31,7 +34,7 @@ You will need a functioning [Go installation][Google Go] for this to work.
Grab the Inbucket source code and compile the daemon: Grab the Inbucket source code and compile the daemon:
go get -v github.com/jhillyerd/inbucket go get -v github.com/jhillyerd/inbucket/cmd/inbucket
Edit etc/inbucket.conf and tailor to your environment. It should work on most Edit etc/inbucket.conf and tailor to your environment. It should work on most
Unix and OS X machines as is. Launch the daemon: Unix and OS X machines as is. Launch the daemon:

View File

@@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"github.com/google/subcommands" "github.com/google/subcommands"
"github.com/jhillyerd/inbucket/rest/client" "github.com/jhillyerd/inbucket/pkg/rest/client"
) )
type listCmd struct { type listCmd struct {

View File

@@ -10,7 +10,7 @@ import (
"time" "time"
"github.com/google/subcommands" "github.com/google/subcommands"
"github.com/jhillyerd/inbucket/rest/client" "github.com/jhillyerd/inbucket/pkg/rest/client"
) )
type matchCmd struct { type matchCmd struct {

View File

@@ -7,7 +7,7 @@ import (
"os" "os"
"github.com/google/subcommands" "github.com/google/subcommands"
"github.com/jhillyerd/inbucket/rest/client" "github.com/jhillyerd/inbucket/pkg/rest/client"
) )
type mboxCmd struct { type mboxCmd struct {

View File

@@ -12,15 +12,15 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/pkg/config"
"github.com/jhillyerd/inbucket/filestore" "github.com/jhillyerd/inbucket/pkg/log"
"github.com/jhillyerd/inbucket/httpd" "github.com/jhillyerd/inbucket/pkg/msghub"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/rest"
"github.com/jhillyerd/inbucket/msghub" "github.com/jhillyerd/inbucket/pkg/server/pop3"
"github.com/jhillyerd/inbucket/pop3d" "github.com/jhillyerd/inbucket/pkg/server/smtp"
"github.com/jhillyerd/inbucket/rest" "github.com/jhillyerd/inbucket/pkg/server/web"
"github.com/jhillyerd/inbucket/smtpd" "github.com/jhillyerd/inbucket/pkg/storage/file"
"github.com/jhillyerd/inbucket/webui" "github.com/jhillyerd/inbucket/pkg/webui"
) )
var ( var (
@@ -39,8 +39,8 @@ var (
shutdownChan = make(chan bool) shutdownChan = make(chan bool)
// Server instances // Server instances
smtpServer *smtpd.Server smtpServer *smtp.Server
pop3Server *pop3d.Server pop3Server *pop3.Server
) )
func init() { func init() {
@@ -119,17 +119,17 @@ func main() {
ds := filestore.DefaultFileDataStore() ds := filestore.DefaultFileDataStore()
// Start HTTP server // Start HTTP server
httpd.Initialize(config.GetWebConfig(), shutdownChan, ds, msgHub) web.Initialize(config.GetWebConfig(), shutdownChan, ds, msgHub)
webui.SetupRoutes(httpd.Router) webui.SetupRoutes(web.Router)
rest.SetupRoutes(httpd.Router) rest.SetupRoutes(web.Router)
go httpd.Start(rootCtx) go web.Start(rootCtx)
// Start POP3 server // Start POP3 server
pop3Server = pop3d.New(config.GetPOP3Config(), shutdownChan, ds) pop3Server = pop3.New(config.GetPOP3Config(), shutdownChan, ds)
go pop3Server.Start(rootCtx) go pop3Server.Start(rootCtx)
// Startup SMTP server // Startup SMTP server
smtpServer = smtpd.NewServer(config.GetSMTPConfig(), shutdownChan, ds, msgHub) smtpServer = smtp.NewServer(config.GetSMTPConfig(), shutdownChan, ds, msgHub)
go smtpServer.Start(rootCtx) go smtpServer.Start(rootCtx)
// Loop forever waiting for signals or shutdown channel // Loop forever waiting for signals or shutdown channel

View File

@@ -1,19 +0,0 @@
package datastore
import (
"strconv"
"sync"
)
type HashLock [4096]sync.RWMutex
func (h *HashLock) Get(hash string) *sync.RWMutex {
if len(hash) < 3 {
return nil
}
i, err := strconv.ParseInt(hash[0:3], 16, 0)
if err != nil {
return nil
}
return &h[i]
}

View File

@@ -16,8 +16,6 @@ apk add --no-cache --virtual .build-deps git
# Setup # Setup
export GOBIN="$bindir" export GOBIN="$bindir"
cd "$srcdir" cd "$srcdir"
# Fetch tags for describe
git fetch -t
builddate="$(date -Iseconds)" builddate="$(date -Iseconds)"
buildver="$(git describe --tags --always)" buildver="$(git describe --tags --always)"
@@ -30,7 +28,7 @@ echo "### Testing Inbucket"
go test ./... go test ./...
echo "### Building Inbucket" echo "### Building Inbucket"
go build -o inbucket -ldflags "-X 'main.version=$buildver' -X 'main.date=$builddate'" -v . go build -o inbucket -ldflags "-X 'main.version=$buildver' -X 'main.date=$builddate'" -v ./cmd/inbucket
echo "### Installing Inbucket" echo "### Installing Inbucket"
set -x set -x

View File

@@ -128,7 +128,7 @@ func openLogFile() error {
var err error var err error
logf, err = os.OpenFile(logfname, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) logf, err = os.OpenFile(logfname, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
if err != nil { if err != nil {
return fmt.Errorf("Failed to create %v: %v\n", logfname, err) return fmt.Errorf("failed to create %v: %v", logfname, err)
} }
golog.SetOutput(logf) golog.SetOutput(logf)
Tracef("Opened new logfile") Tracef("Opened new logfile")

View File

@@ -10,15 +10,15 @@ import (
"io/ioutil" "io/ioutil"
"strconv" "strconv"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/log"
"github.com/jhillyerd/inbucket/httpd" "github.com/jhillyerd/inbucket/pkg/rest/model"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/server/web"
"github.com/jhillyerd/inbucket/rest/model" "github.com/jhillyerd/inbucket/pkg/storage"
"github.com/jhillyerd/inbucket/stringutil" "github.com/jhillyerd/inbucket/pkg/stringutil"
) )
// MailboxListV1 renders a list of messages in a mailbox // MailboxListV1 renders a list of messages in a mailbox
func MailboxListV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func MailboxListV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404 // Don't have to validate these aren't empty, Gorilla returns 404
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil { if err != nil {
@@ -48,11 +48,11 @@ func MailboxListV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context)
Size: msg.Size(), Size: msg.Size(),
} }
} }
return httpd.RenderJSON(w, jmessages) return web.RenderJSON(w, jmessages)
} }
// MailboxShowV1 renders a particular message from a mailbox // MailboxShowV1 renders a particular message from a mailbox
func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404 // Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"] id := ctx.Vars["id"]
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
@@ -96,7 +96,7 @@ func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context)
} }
} }
return httpd.RenderJSON(w, return web.RenderJSON(w,
&model.JSONMessageV1{ &model.JSONMessageV1{
Mailbox: name, Mailbox: name,
ID: msg.ID(), ID: msg.ID(),
@@ -115,7 +115,7 @@ func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context)
} }
// MailboxPurgeV1 deletes all messages from a mailbox // MailboxPurgeV1 deletes all messages from a mailbox
func MailboxPurgeV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func MailboxPurgeV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404 // Don't have to validate these aren't empty, Gorilla returns 404
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil { if err != nil {
@@ -133,11 +133,11 @@ func MailboxPurgeV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context
} }
log.Tracef("HTTP purged mailbox for %q", name) log.Tracef("HTTP purged mailbox for %q", name)
return httpd.RenderJSON(w, "OK") return web.RenderJSON(w, "OK")
} }
// MailboxSourceV1 displays the raw source of a message, including headers. Renders text/plain // MailboxSourceV1 displays the raw source of a message, including headers. Renders text/plain
func MailboxSourceV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func MailboxSourceV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404 // Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"] id := ctx.Vars["id"]
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
@@ -171,7 +171,7 @@ func MailboxSourceV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Contex
} }
// MailboxDeleteV1 removes a particular message from a mailbox // MailboxDeleteV1 removes a particular message from a mailbox
func MailboxDeleteV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func MailboxDeleteV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404 // Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"] id := ctx.Vars["id"]
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
@@ -197,5 +197,5 @@ func MailboxDeleteV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Contex
return fmt.Errorf("Delete(%q) failed: %v", id, err) return fmt.Errorf("Delete(%q) failed: %v", id, err)
} }
return httpd.RenderJSON(w, "OK") return web.RenderJSON(w, "OK")
} }

View File

@@ -9,7 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/storage"
) )
const ( const (

View File

@@ -8,7 +8,7 @@ import (
"net/url" "net/url"
"time" "time"
"github.com/jhillyerd/inbucket/rest/model" "github.com/jhillyerd/inbucket/pkg/rest/model"
) )
// Client accesses the Inbucket REST API v1 // Client accesses the Inbucket REST API v1

View File

@@ -30,6 +30,7 @@ type JSONMessageV1 struct {
Attachments []*JSONMessageAttachmentV1 `json:"attachments"` Attachments []*JSONMessageAttachmentV1 `json:"attachments"`
} }
// JSONMessageAttachmentV1 contains information about a MIME attachment
type JSONMessageAttachmentV1 struct { type JSONMessageAttachmentV1 struct {
FileName string `json:"filename"` FileName string `json:"filename"`
ContentType string `json:"content-type"` ContentType string `json:"content-type"`

23
pkg/rest/routes.go Normal file
View File

@@ -0,0 +1,23 @@
package rest
import "github.com/gorilla/mux"
import "github.com/jhillyerd/inbucket/pkg/server/web"
// SetupRoutes populates the routes for the REST interface
func SetupRoutes(r *mux.Router) {
// API v1
r.Path("/api/v1/mailbox/{name}").Handler(
web.Handler(MailboxListV1)).Name("MailboxListV1").Methods("GET")
r.Path("/api/v1/mailbox/{name}").Handler(
web.Handler(MailboxPurgeV1)).Name("MailboxPurgeV1").Methods("DELETE")
r.Path("/api/v1/mailbox/{name}/{id}").Handler(
web.Handler(MailboxShowV1)).Name("MailboxShowV1").Methods("GET")
r.Path("/api/v1/mailbox/{name}/{id}").Handler(
web.Handler(MailboxDeleteV1)).Name("MailboxDeleteV1").Methods("DELETE")
r.Path("/api/v1/mailbox/{name}/{id}/source").Handler(
web.Handler(MailboxSourceV1)).Name("MailboxSourceV1").Methods("GET")
r.Path("/api/v1/monitor/messages").Handler(
web.Handler(MonitorAllMessagesV1)).Name("MonitorAllMessagesV1").Methods("GET")
r.Path("/api/v1/monitor/messages/{name}").Handler(
web.Handler(MonitorMailboxMessagesV1)).Name("MonitorMailboxMessagesV1").Methods("GET")
}

View File

@@ -5,11 +5,11 @@ import (
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/jhillyerd/inbucket/httpd" "github.com/jhillyerd/inbucket/pkg/log"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/msghub"
"github.com/jhillyerd/inbucket/msghub" "github.com/jhillyerd/inbucket/pkg/rest/model"
"github.com/jhillyerd/inbucket/rest/model" "github.com/jhillyerd/inbucket/pkg/server/web"
"github.com/jhillyerd/inbucket/stringutil" "github.com/jhillyerd/inbucket/pkg/stringutil"
) )
const ( const (
@@ -144,17 +144,19 @@ func (ml *msgListener) Close() {
} }
} }
// MonitorAllMessagesV1 is a web handler which upgrades the connection to a websocket and notifies
// the client of all messages received.
func MonitorAllMessagesV1( func MonitorAllMessagesV1(
w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Upgrade to Websocket // Upgrade to Websocket
conn, err := upgrader.Upgrade(w, req, nil) conn, err := upgrader.Upgrade(w, req, nil)
if err != nil { if err != nil {
return err return err
} }
httpd.ExpWebSocketConnectsCurrent.Add(1) web.ExpWebSocketConnectsCurrent.Add(1)
defer func() { defer func() {
_ = conn.Close() _ = conn.Close()
httpd.ExpWebSocketConnectsCurrent.Add(-1) web.ExpWebSocketConnectsCurrent.Add(-1)
}() }()
log.Tracef("HTTP[%v] Upgraded to websocket", req.RemoteAddr) log.Tracef("HTTP[%v] Upgraded to websocket", req.RemoteAddr)
@@ -167,8 +169,10 @@ func MonitorAllMessagesV1(
return nil return nil
} }
// MonitorMailboxMessagesV1 is a web handler which upgrades the connection to a websocket and
// notifies the client of messages received by a particular mailbox.
func MonitorMailboxMessagesV1( func MonitorMailboxMessagesV1(
w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil { if err != nil {
return err return err
@@ -178,10 +182,10 @@ func MonitorMailboxMessagesV1(
if err != nil { if err != nil {
return err return err
} }
httpd.ExpWebSocketConnectsCurrent.Add(1) web.ExpWebSocketConnectsCurrent.Add(1)
defer func() { defer func() {
_ = conn.Close() _ = conn.Close()
httpd.ExpWebSocketConnectsCurrent.Add(-1) web.ExpWebSocketConnectsCurrent.Add(-1)
}() }()
log.Tracef("HTTP[%v] Upgraded to websocket", req.RemoteAddr) log.Tracef("HTTP[%v] Upgraded to websocket", req.RemoteAddr)

View File

@@ -10,10 +10,10 @@ import (
"time" "time"
"github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime"
"github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/pkg/config"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/msghub"
"github.com/jhillyerd/inbucket/httpd" "github.com/jhillyerd/inbucket/pkg/server/web"
"github.com/jhillyerd/inbucket/msghub" "github.com/jhillyerd/inbucket/pkg/storage"
) )
type InputMessageData struct { type InputMessageData struct {
@@ -184,7 +184,7 @@ func testRestGet(url string) (*httptest.ResponseRecorder, error) {
} }
w := httptest.NewRecorder() w := httptest.NewRecorder()
httpd.Router.ServeHTTP(w, req) web.Router.ServeHTTP(w, req)
return w, nil return w, nil
} }
@@ -200,8 +200,8 @@ func setupWebServer(ds datastore.DataStore) *bytes.Buffer {
PublicDir: "../themes/bootstrap/public", PublicDir: "../themes/bootstrap/public",
} }
shutdownChan := make(chan bool) shutdownChan := make(chan bool)
httpd.Initialize(cfg, shutdownChan, ds, &msghub.Hub{}) web.Initialize(cfg, shutdownChan, ds, &msghub.Hub{})
SetupRoutes(httpd.Router) SetupRoutes(web.Router)
return buf return buf
} }

View File

@@ -1,4 +1,4 @@
package pop3d package pop3
import ( import (
"bufio" "bufio"
@@ -11,8 +11,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/log"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/storage"
) )
// State tracks the current mode of our POP3 state machine // State tracks the current mode of our POP3 state machine

View File

@@ -1,4 +1,4 @@
package pop3d package pop3
import ( import (
"context" "context"
@@ -7,9 +7,9 @@ import (
"sync" "sync"
"time" "time"
"github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/pkg/config"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/log"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/storage"
) )
// Server defines an instance of our POP3 server // Server defines an instance of our POP3 server

View File

@@ -1,4 +1,4 @@
package smtpd package smtp
import ( import (
"bufio" "bufio"
@@ -12,10 +12,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/log"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/msghub"
"github.com/jhillyerd/inbucket/msghub" "github.com/jhillyerd/inbucket/pkg/storage"
"github.com/jhillyerd/inbucket/stringutil" "github.com/jhillyerd/inbucket/pkg/stringutil"
) )
// State tracks the current mode of our SMTP state machine // State tracks the current mode of our SMTP state machine

View File

@@ -1,4 +1,4 @@
package smtpd package smtp
import ( import (
"bytes" "bytes"
@@ -13,9 +13,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/pkg/config"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/msghub"
"github.com/jhillyerd/inbucket/msghub" "github.com/jhillyerd/inbucket/pkg/storage"
) )
type scriptStep struct { type scriptStep struct {

View File

@@ -1,4 +1,4 @@
package smtpd package smtp
import ( import (
"container/list" "container/list"
@@ -10,10 +10,10 @@ import (
"sync" "sync"
"time" "time"
"github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/pkg/config"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/log"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/msghub"
"github.com/jhillyerd/inbucket/msghub" "github.com/jhillyerd/inbucket/pkg/storage"
) )
func init() { func init() {

View File

@@ -1,4 +1,4 @@
package httpd package web
import ( import (
"net/http" "net/http"
@@ -6,9 +6,9 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/pkg/config"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/msghub"
"github.com/jhillyerd/inbucket/msghub" "github.com/jhillyerd/inbucket/pkg/storage"
) )
// Context is passed into every request handler function // Context is passed into every request handler function

View File

@@ -1,4 +1,4 @@
package httpd package web
import ( import (
"fmt" "fmt"
@@ -8,7 +8,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/log"
) )
// TemplateFuncs declares functions made available to all templates (including partials) // TemplateFuncs declares functions made available to all templates (including partials)

View File

@@ -1,4 +1,4 @@
package httpd package web
import ( import (
"html/template" "html/template"

View File

@@ -1,4 +1,4 @@
package httpd package web
import ( import (
"encoding/json" "encoding/json"

View File

@@ -1,5 +1,5 @@
// Package httpd provides the plumbing for Inbucket's web GUI and RESTful API // Package web provides the plumbing for Inbucket's web GUI and RESTful API
package httpd package web
import ( import (
"context" "context"
@@ -12,10 +12,10 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/securecookie" "github.com/gorilla/securecookie"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/pkg/config"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/log"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/msghub"
"github.com/jhillyerd/inbucket/msghub" "github.com/jhillyerd/inbucket/pkg/storage"
) )
// Handler is a function type that handles an HTTP request in Inbucket // Handler is a function type that handles an HTTP request in Inbucket

View File

@@ -1,4 +1,4 @@
package httpd package web
import ( import (
"html/template" "html/template"
@@ -8,7 +8,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/log"
) )
var cachedMutex sync.Mutex var cachedMutex sync.Mutex

View File

@@ -11,8 +11,8 @@ import (
"time" "time"
"github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/log"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/storage"
) )
// FileMessage implements Message and contains a little bit of data about a // FileMessage implements Message and contains a little bit of data about a

View File

@@ -11,10 +11,10 @@ import (
"sync" "sync"
"time" "time"
"github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/pkg/config"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/log"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/storage"
"github.com/jhillyerd/inbucket/stringutil" "github.com/jhillyerd/inbucket/pkg/stringutil"
) )
// Name of index file in each mailbox // Name of index file in each mailbox
@@ -140,6 +140,7 @@ func (ds *FileDataStore) AllMailboxes() ([]datastore.Mailbox, error) {
return mailboxes, nil return mailboxes, nil
} }
// LockFor returns the RWMutex for this mailbox, or an error.
func (ds *FileDataStore) LockFor(emailAddress string) (*sync.RWMutex, error) { func (ds *FileDataStore) LockFor(emailAddress string) (*sync.RWMutex, error) {
name, err := stringutil.ParseMailboxName(emailAddress) name, err := stringutil.ParseMailboxName(emailAddress)
if err != nil { if err != nil {

View File

@@ -11,7 +11,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/pkg/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

23
pkg/storage/lock.go Normal file
View File

@@ -0,0 +1,23 @@
package datastore
import (
"strconv"
"sync"
)
// HashLock holds a fixed length array of mutexes. This approach allows concurrent mailbox
// access in most cases without requiring an infinite number of mutexes.
type HashLock [4096]sync.RWMutex
// Get returns a RWMutex based on the first 12 bits of the mailbox hash. Hash must be a hexidecimal
// string of three or more characters.
func (h *HashLock) Get(hash string) *sync.RWMutex {
if len(hash) < 3 {
return nil
}
i, err := strconv.ParseInt(hash[0:3], 16, 0)
if err != nil {
return nil
}
return &h[i]
}

View File

@@ -3,7 +3,7 @@ package datastore_test
import ( import (
"testing" "testing"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/storage"
) )
func TestHashLock(t *testing.T) { func TestHashLock(t *testing.T) {

View File

@@ -6,8 +6,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/pkg/config"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/log"
) )
var ( var (

View File

@@ -27,6 +27,7 @@ func (m *MockDataStore) AllMailboxes() ([]Mailbox, error) {
return args.Get(0).([]Mailbox), args.Error(1) return args.Get(0).([]Mailbox), args.Error(1)
} }
// LockFor mock function returns a new RWMutex, never errors.
func (m *MockDataStore) LockFor(name string) (*sync.RWMutex, error) { func (m *MockDataStore) LockFor(name string) (*sync.RWMutex, error) {
return &sync.RWMutex{}, nil return &sync.RWMutex{}, nil
} }

View File

@@ -7,29 +7,29 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"github.com/jhillyerd/inbucket/datastore" "github.com/jhillyerd/inbucket/pkg/log"
"github.com/jhillyerd/inbucket/httpd" "github.com/jhillyerd/inbucket/pkg/server/web"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pkg/storage"
"github.com/jhillyerd/inbucket/sanitize" "github.com/jhillyerd/inbucket/pkg/stringutil"
"github.com/jhillyerd/inbucket/stringutil" "github.com/jhillyerd/inbucket/pkg/webui/sanitize"
) )
// MailboxIndex renders the index page for a particular mailbox // MailboxIndex renders the index page for a particular mailbox
func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Form values must be validated manually // Form values must be validated manually
name := req.FormValue("name") name := req.FormValue("name")
selected := req.FormValue("id") selected := req.FormValue("id")
if len(name) == 0 { if len(name) == 0 {
ctx.Session.AddFlash("Account name is required", "errors") ctx.Session.AddFlash("Account name is required", "errors")
_ = ctx.Session.Save(req, w) _ = ctx.Session.Save(req, w)
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
name, err = stringutil.ParseMailboxName(name) name, err = stringutil.ParseMailboxName(name)
if err != nil { if err != nil {
ctx.Session.AddFlash(err.Error(), "errors") ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w) _ = ctx.Session.Save(req, w)
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
// Remember this mailbox was visited // Remember this mailbox was visited
@@ -40,7 +40,7 @@ func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *httpd.Context)
return err return err
} }
// Render template // Render template
return httpd.RenderTemplate("mailbox/index.html", w, map[string]interface{}{ return web.RenderTemplate("mailbox/index.html", w, map[string]interface{}{
"ctx": ctx, "ctx": ctx,
"errorFlash": errorFlash, "errorFlash": errorFlash,
"name": name, "name": name,
@@ -49,24 +49,24 @@ func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *httpd.Context)
} }
// MailboxLink handles pretty links to a particular message. Renders a redirect // MailboxLink handles pretty links to a particular message. Renders a redirect
func MailboxLink(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func MailboxLink(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404 // Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"] id := ctx.Vars["id"]
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil { if err != nil {
ctx.Session.AddFlash(err.Error(), "errors") ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w) _ = ctx.Session.Save(req, w)
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
// Build redirect // Build redirect
uri := fmt.Sprintf("%s?name=%s&id=%s", httpd.Reverse("MailboxIndex"), name, id) uri := fmt.Sprintf("%s?name=%s&id=%s", web.Reverse("MailboxIndex"), name, id)
http.Redirect(w, req, uri, http.StatusSeeOther) http.Redirect(w, req, uri, http.StatusSeeOther)
return nil return nil
} }
// MailboxList renders a list of messages in a mailbox. Renders a partial // MailboxList renders a list of messages in a mailbox. Renders a partial
func MailboxList(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func MailboxList(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404 // Don't have to validate these aren't empty, Gorilla returns 404
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil { if err != nil {
@@ -84,7 +84,7 @@ func MailboxList(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
} }
log.Tracef("Got %v messsages", len(messages)) log.Tracef("Got %v messsages", len(messages))
// Render partial template // Render partial template
return httpd.RenderPartial("mailbox/_list.html", w, map[string]interface{}{ return web.RenderPartial("mailbox/_list.html", w, map[string]interface{}{
"ctx": ctx, "ctx": ctx,
"name": name, "name": name,
"messages": messages, "messages": messages,
@@ -92,7 +92,7 @@ func MailboxList(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
} }
// MailboxShow renders a particular message from a mailbox. Renders an HTML partial // MailboxShow renders a particular message from a mailbox. Renders an HTML partial
func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404 // Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"] id := ctx.Vars["id"]
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
@@ -117,7 +117,7 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
if err != nil { if err != nil {
return fmt.Errorf("ReadBody(%q) failed: %v", id, err) return fmt.Errorf("ReadBody(%q) failed: %v", id, err)
} }
body := template.HTML(httpd.TextToHTML(mime.Text)) body := template.HTML(web.TextToHTML(mime.Text))
htmlAvailable := mime.HTML != "" htmlAvailable := mime.HTML != ""
var htmlBody template.HTML var htmlBody template.HTML
if htmlAvailable { if htmlAvailable {
@@ -128,7 +128,7 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
} }
} }
// Render partial template // Render partial template
return httpd.RenderPartial("mailbox/_show.html", w, map[string]interface{}{ return web.RenderPartial("mailbox/_show.html", w, map[string]interface{}{
"ctx": ctx, "ctx": ctx,
"name": name, "name": name,
"message": msg, "message": msg,
@@ -141,7 +141,7 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
} }
// MailboxHTML displays the HTML content of a message. Renders a partial // MailboxHTML displays the HTML content of a message. Renders a partial
func MailboxHTML(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func MailboxHTML(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404 // Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"] id := ctx.Vars["id"]
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
@@ -168,7 +168,7 @@ func MailboxHTML(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
} }
// Render partial template // Render partial template
w.Header().Set("Content-Type", "text/html; charset=UTF-8") w.Header().Set("Content-Type", "text/html; charset=UTF-8")
return httpd.RenderPartial("mailbox/_html.html", w, map[string]interface{}{ return web.RenderPartial("mailbox/_html.html", w, map[string]interface{}{
"ctx": ctx, "ctx": ctx,
"name": name, "name": name,
"message": message, "message": message,
@@ -178,7 +178,7 @@ func MailboxHTML(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
} }
// MailboxSource displays the raw source of a message, including headers. Renders text/plain // MailboxSource displays the raw source of a message, including headers. Renders text/plain
func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404 // Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"] id := ctx.Vars["id"]
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
@@ -213,14 +213,14 @@ func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *httpd.Context)
// MailboxDownloadAttach sends the attachment to the client; disposition: // MailboxDownloadAttach sends the attachment to the client; disposition:
// attachment, type: application/octet-stream // attachment, type: application/octet-stream
func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404 // Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"] id := ctx.Vars["id"]
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil { if err != nil {
ctx.Session.AddFlash(err.Error(), "errors") ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w) _ = ctx.Session.Save(req, w)
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
numStr := ctx.Vars["num"] numStr := ctx.Vars["num"]
@@ -228,7 +228,7 @@ func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *httpd.
if err != nil { if err != nil {
ctx.Session.AddFlash("Attachment number must be unsigned numeric", "errors") ctx.Session.AddFlash("Attachment number must be unsigned numeric", "errors")
_ = ctx.Session.Save(req, w) _ = ctx.Session.Save(req, w)
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
mb, err := ctx.DataStore.MailboxFor(name) mb, err := ctx.DataStore.MailboxFor(name)
@@ -252,7 +252,7 @@ func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *httpd.
if int(num) >= len(body.Attachments) { if int(num) >= len(body.Attachments) {
ctx.Session.AddFlash("Attachment number too high", "errors") ctx.Session.AddFlash("Attachment number too high", "errors")
_ = ctx.Session.Save(req, w) _ = ctx.Session.Save(req, w)
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
part := body.Attachments[num] part := body.Attachments[num]
@@ -266,13 +266,13 @@ func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *httpd.
} }
// MailboxViewAttach sends the attachment to the client for online viewing // MailboxViewAttach sends the attachment to the client for online viewing
func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404 // Don't have to validate these aren't empty, Gorilla returns 404
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil { if err != nil {
ctx.Session.AddFlash(err.Error(), "errors") ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w) _ = ctx.Session.Save(req, w)
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
id := ctx.Vars["id"] id := ctx.Vars["id"]
@@ -281,7 +281,7 @@ func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *httpd.Cont
if err != nil { if err != nil {
ctx.Session.AddFlash("Attachment number must be unsigned numeric", "errors") ctx.Session.AddFlash("Attachment number must be unsigned numeric", "errors")
_ = ctx.Session.Save(req, w) _ = ctx.Session.Save(req, w)
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
mb, err := ctx.DataStore.MailboxFor(name) mb, err := ctx.DataStore.MailboxFor(name)
@@ -305,7 +305,7 @@ func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *httpd.Cont
if int(num) >= len(body.Attachments) { if int(num) >= len(body.Attachments) {
ctx.Session.AddFlash("Attachment number too high", "errors") ctx.Session.AddFlash("Attachment number too high", "errors")
_ = ctx.Session.Save(req, w) _ = ctx.Session.Save(req, w)
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
part := body.Attachments[num] part := body.Attachments[num]

View File

@@ -1,7 +1,7 @@
package webui package webui
import ( import (
"github.com/jhillyerd/inbucket/httpd" "github.com/jhillyerd/inbucket/pkg/server/web"
) )
const ( const (
@@ -12,7 +12,7 @@ const (
) )
// RememberMailbox manages the list of recently accessed mailboxes stored in the session // RememberMailbox manages the list of recently accessed mailboxes stored in the session
func RememberMailbox(ctx *httpd.Context, mailbox string) { func RememberMailbox(ctx *web.Context, mailbox string) {
recent := RecentMailboxes(ctx) recent := RecentMailboxes(ctx)
newRecent := make([]string, 1, maxRemembered) newRecent := make([]string, 1, maxRemembered)
newRecent[0] = mailbox newRecent[0] = mailbox
@@ -28,7 +28,7 @@ func RememberMailbox(ctx *httpd.Context, mailbox string) {
} }
// RecentMailboxes returns a slice of the most recently accessed mailboxes // RecentMailboxes returns a slice of the most recently accessed mailboxes
func RecentMailboxes(ctx *httpd.Context) []string { func RecentMailboxes(ctx *web.Context) []string {
val := ctx.Session.Values[mailboxKey] val := ctx.Session.Values[mailboxKey]
recent, _ := val.([]string) recent, _ := val.([]string)
return recent return recent

View File

@@ -6,13 +6,13 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/pkg/config"
"github.com/jhillyerd/inbucket/httpd" "github.com/jhillyerd/inbucket/pkg/server/web"
"github.com/jhillyerd/inbucket/stringutil" "github.com/jhillyerd/inbucket/pkg/stringutil"
) )
// RootIndex serves the Inbucket landing page // RootIndex serves the Inbucket landing page
func RootIndex(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func RootIndex(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
greeting, err := ioutil.ReadFile(config.GetWebConfig().GreetingFile) greeting, err := ioutil.ReadFile(config.GetWebConfig().GreetingFile)
if err != nil { if err != nil {
return fmt.Errorf("Failed to load greeting: %v", err) return fmt.Errorf("Failed to load greeting: %v", err)
@@ -23,7 +23,7 @@ func RootIndex(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (er
return err return err
} }
// Render template // Render template
return httpd.RenderTemplate("root/index.html", w, map[string]interface{}{ return web.RenderTemplate("root/index.html", w, map[string]interface{}{
"ctx": ctx, "ctx": ctx,
"errorFlash": errorFlash, "errorFlash": errorFlash,
"greeting": template.HTML(string(greeting)), "greeting": template.HTML(string(greeting)),
@@ -31,11 +31,11 @@ func RootIndex(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (er
} }
// RootMonitor serves the Inbucket monitor page // RootMonitor serves the Inbucket monitor page
func RootMonitor(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func RootMonitor(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
if !config.GetWebConfig().MonitorVisible { if !config.GetWebConfig().MonitorVisible {
ctx.Session.AddFlash("Monitor is disabled in configuration", "errors") ctx.Session.AddFlash("Monitor is disabled in configuration", "errors")
_ = ctx.Session.Save(req, w) _ = ctx.Session.Save(req, w)
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
// Get flash messages, save session // Get flash messages, save session
@@ -44,25 +44,25 @@ func RootMonitor(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
return err return err
} }
// Render template // Render template
return httpd.RenderTemplate("root/monitor.html", w, map[string]interface{}{ return web.RenderTemplate("root/monitor.html", w, map[string]interface{}{
"ctx": ctx, "ctx": ctx,
"errorFlash": errorFlash, "errorFlash": errorFlash,
}) })
} }
// RootMonitorMailbox serves the Inbucket monitor page for a particular mailbox // RootMonitorMailbox serves the Inbucket monitor page for a particular mailbox
func RootMonitorMailbox(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func RootMonitorMailbox(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
if !config.GetWebConfig().MonitorVisible { if !config.GetWebConfig().MonitorVisible {
ctx.Session.AddFlash("Monitor is disabled in configuration", "errors") ctx.Session.AddFlash("Monitor is disabled in configuration", "errors")
_ = ctx.Session.Save(req, w) _ = ctx.Session.Save(req, w)
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
name, err := stringutil.ParseMailboxName(ctx.Vars["name"]) name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil { if err != nil {
ctx.Session.AddFlash(err.Error(), "errors") ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w) _ = ctx.Session.Save(req, w)
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
// Get flash messages, save session // Get flash messages, save session
@@ -71,7 +71,7 @@ func RootMonitorMailbox(w http.ResponseWriter, req *http.Request, ctx *httpd.Con
return err return err
} }
// Render template // Render template
return httpd.RenderTemplate("root/monitor.html", w, map[string]interface{}{ return web.RenderTemplate("root/monitor.html", w, map[string]interface{}{
"ctx": ctx, "ctx": ctx,
"errorFlash": errorFlash, "errorFlash": errorFlash,
"name": name, "name": name,
@@ -79,7 +79,7 @@ func RootMonitorMailbox(w http.ResponseWriter, req *http.Request, ctx *httpd.Con
} }
// RootStatus serves the Inbucket status page // RootStatus serves the Inbucket status page
func RootStatus(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { func RootStatus(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
smtpListener := fmt.Sprintf("%s:%d", config.GetSMTPConfig().IP4address.String(), smtpListener := fmt.Sprintf("%s:%d", config.GetSMTPConfig().IP4address.String(),
config.GetSMTPConfig().IP4port) config.GetSMTPConfig().IP4port)
pop3Listener := fmt.Sprintf("%s:%d", config.GetPOP3Config().IP4address.String(), pop3Listener := fmt.Sprintf("%s:%d", config.GetPOP3Config().IP4address.String(),
@@ -92,7 +92,7 @@ func RootStatus(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (e
return err return err
} }
// Render template // Render template
return httpd.RenderTemplate("root/status.html", w, map[string]interface{}{ return web.RenderTemplate("root/status.html", w, map[string]interface{}{
"ctx": ctx, "ctx": ctx,
"errorFlash": errorFlash, "errorFlash": errorFlash,
"version": config.Version, "version": config.Version,

35
pkg/webui/routes.go Normal file
View File

@@ -0,0 +1,35 @@
// Package webui powers Inbucket's web GUI
package webui
import (
"github.com/gorilla/mux"
"github.com/jhillyerd/inbucket/pkg/server/web"
)
// SetupRoutes populates routes for the webui into the provided Router
func SetupRoutes(r *mux.Router) {
r.Path("/").Handler(
web.Handler(RootIndex)).Name("RootIndex").Methods("GET")
r.Path("/monitor").Handler(
web.Handler(RootMonitor)).Name("RootMonitor").Methods("GET")
r.Path("/monitor/{name}").Handler(
web.Handler(RootMonitorMailbox)).Name("RootMonitorMailbox").Methods("GET")
r.Path("/status").Handler(
web.Handler(RootStatus)).Name("RootStatus").Methods("GET")
r.Path("/link/{name}/{id}").Handler(
web.Handler(MailboxLink)).Name("MailboxLink").Methods("GET")
r.Path("/mailbox").Handler(
web.Handler(MailboxIndex)).Name("MailboxIndex").Methods("GET")
r.Path("/mailbox/{name}").Handler(
web.Handler(MailboxList)).Name("MailboxList").Methods("GET")
r.Path("/mailbox/{name}/{id}").Handler(
web.Handler(MailboxShow)).Name("MailboxShow").Methods("GET")
r.Path("/mailbox/{name}/{id}/html").Handler(
web.Handler(MailboxHTML)).Name("MailboxHtml").Methods("GET")
r.Path("/mailbox/{name}/{id}/source").Handler(
web.Handler(MailboxSource)).Name("MailboxSource").Methods("GET")
r.Path("/mailbox/dattach/{name}/{id}/{num}/{file}").Handler(
web.Handler(MailboxDownloadAttach)).Name("MailboxDownloadAttach").Methods("GET")
r.Path("/mailbox/vattach/{name}/{id}/{num}/{file}").Handler(
web.Handler(MailboxViewAttach)).Name("MailboxViewAttach").Methods("GET")
}

View File

@@ -18,6 +18,7 @@ var (
AllowAttrs("style").Matching(cssSafe).Globally() AllowAttrs("style").Matching(cssSafe).Globally()
) )
// HTML sanitizes the provided html, while attempting to preserve inline CSS styling.
func HTML(html string) (output string, err error) { func HTML(html string) (output string, err error) {
output, err = sanitizeStyleTags(html) output, err = sanitizeStyleTags(html)
if err != nil { if err != nil {

View File

@@ -3,7 +3,7 @@ package sanitize_test
import ( import (
"testing" "testing"
"github.com/jhillyerd/inbucket/sanitize" "github.com/jhillyerd/inbucket/pkg/webui/sanitize"
) )
// TestHTMLPlainStrings test plain text passthrough // TestHTMLPlainStrings test plain text passthrough

View File

@@ -1,23 +0,0 @@
package rest
import "github.com/gorilla/mux"
import "github.com/jhillyerd/inbucket/httpd"
// SetupRoutes populates the routes for the REST interface
func SetupRoutes(r *mux.Router) {
// API v1
r.Path("/api/v1/mailbox/{name}").Handler(
httpd.Handler(MailboxListV1)).Name("MailboxListV1").Methods("GET")
r.Path("/api/v1/mailbox/{name}").Handler(
httpd.Handler(MailboxPurgeV1)).Name("MailboxPurgeV1").Methods("DELETE")
r.Path("/api/v1/mailbox/{name}/{id}").Handler(
httpd.Handler(MailboxShowV1)).Name("MailboxShowV1").Methods("GET")
r.Path("/api/v1/mailbox/{name}/{id}").Handler(
httpd.Handler(MailboxDeleteV1)).Name("MailboxDeleteV1").Methods("DELETE")
r.Path("/api/v1/mailbox/{name}/{id}/source").Handler(
httpd.Handler(MailboxSourceV1)).Name("MailboxSourceV1").Methods("GET")
r.Path("/api/v1/monitor/messages").Handler(
httpd.Handler(MonitorAllMessagesV1)).Name("MonitorAllMessagesV1").Methods("GET")
r.Path("/api/v1/monitor/messages/{name}").Handler(
httpd.Handler(MonitorMailboxMessagesV1)).Name("MonitorMailboxMessagesV1").Methods("GET")
}

View File

@@ -1,35 +0,0 @@
// Package webui powers Inbucket's web GUI
package webui
import (
"github.com/gorilla/mux"
"github.com/jhillyerd/inbucket/httpd"
)
// SetupRoutes populates routes for the webui into the provided Router
func SetupRoutes(r *mux.Router) {
r.Path("/").Handler(
httpd.Handler(RootIndex)).Name("RootIndex").Methods("GET")
r.Path("/monitor").Handler(
httpd.Handler(RootMonitor)).Name("RootMonitor").Methods("GET")
r.Path("/monitor/{name}").Handler(
httpd.Handler(RootMonitorMailbox)).Name("RootMonitorMailbox").Methods("GET")
r.Path("/status").Handler(
httpd.Handler(RootStatus)).Name("RootStatus").Methods("GET")
r.Path("/link/{name}/{id}").Handler(
httpd.Handler(MailboxLink)).Name("MailboxLink").Methods("GET")
r.Path("/mailbox").Handler(
httpd.Handler(MailboxIndex)).Name("MailboxIndex").Methods("GET")
r.Path("/mailbox/{name}").Handler(
httpd.Handler(MailboxList)).Name("MailboxList").Methods("GET")
r.Path("/mailbox/{name}/{id}").Handler(
httpd.Handler(MailboxShow)).Name("MailboxShow").Methods("GET")
r.Path("/mailbox/{name}/{id}/html").Handler(
httpd.Handler(MailboxHTML)).Name("MailboxHtml").Methods("GET")
r.Path("/mailbox/{name}/{id}/source").Handler(
httpd.Handler(MailboxSource)).Name("MailboxSource").Methods("GET")
r.Path("/mailbox/dattach/{name}/{id}/{num}/{file}").Handler(
httpd.Handler(MailboxDownloadAttach)).Name("MailboxDownloadAttach").Methods("GET")
r.Path("/mailbox/vattach/{name}/{id}/{num}/{file}").Handler(
httpd.Handler(MailboxViewAttach)).Name("MailboxViewAttach").Methods("GET")
}