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

Release v1.3.0

This commit is contained in:
James Hillyerd
2018-02-28 14:13:24 -08:00
43 changed files with 1307 additions and 515 deletions

4
.gitignore vendored
View File

@@ -29,9 +29,5 @@ _testmain.go
/inbucket
/inbucket.exe
/dist/**
/target/**
/cmd/client/client
/cmd/client/client.exe
# local goxc config
.goxc.local.json

View File

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

View File

@@ -5,13 +5,11 @@ env:
- DEPLOY_WITH_MAJOR="1.9"
before_script:
- go vet ./...
- go get github.com/golang/lint/golint
go:
- 1.8.x
- 1.9.x
script: go test -race -v ./...
- "1.10"
deploy:
provider: script

View File

@@ -4,16 +4,20 @@ 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
--------------------
## [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
[1.2.0-rc2] - 2017-12-15
------------------------
### Added
- `rest/client` types `MessageHeader` and `Message` with convenience methods;
provides a more natural API
@@ -27,8 +31,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
types
- Fixed panic when `monitor.history` set to 0
[1.2.0-rc1] - 2017-01-29
------------------------
## [v1.2.0-rc1] - 2017-01-29
### Added
- Storage of `To:` header in messages (likely breaks existing datastores)
@@ -54,8 +57,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Allow increased local-part length of 128 chars for Mailgun
- RedHat and Ubuntu now use systemd instead of legacy init systems
[1.1.0] - 2016-09-03
--------------------
## [v1.1.0] - 2016-09-03
### Added
- Homebrew inbucket.conf and formula (see README)
@@ -63,8 +65,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
- Log and continue when unable to delete oldest message during cap enforcement
[1.1.0-rc2] - 2016-03-06
------------------------
## [v1.1.0-rc2] - 2016-03-06
### Added
- Message Cap to status page
@@ -74,8 +75,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Shutdown hang in retention scanner
- Display empty subject as `(No Subject)`
[1.1.0-rc1] - 2016-03-04
------------------------
## [v1.1.0-rc1] - 2016-03-04
### Added
- Inbucket now builds with Go 1.5 or 1.6
@@ -89,8 +89,7 @@ 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
@@ -98,30 +97,29 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Add Link button to messages, allows for directing another person to a
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
[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
[Unreleased]: https://github.com/jhillyerd/inbucket/compare/master...develop
[v1.3.0]: https://github.com/jhillyerd/inbucket/compare/1.2.0...1.3.0
[v1.2.0]: https://github.com/jhillyerd/inbucket/compare/1.2.0-rc2...1.2.0
[v1.2.0-rc2]: https://github.com/jhillyerd/inbucket/compare/1.2.0-rc1...1.2.0-rc2
[v1.2.0-rc1]: https://github.com/jhillyerd/inbucket/compare/1.1.0...1.2.0-rc1
[v1.1.0]: https://github.com/jhillyerd/inbucket/compare/1.1.0-rc2...1.1.0
[v1.1.0-rc2]: https://github.com/jhillyerd/inbucket/compare/1.1.0-rc1...1.1.0-rc2
[v1.1.0-rc1]: https://github.com/jhillyerd/inbucket/compare/1.0...1.1.0-rc1
[v1.0]: https://github.com/jhillyerd/inbucket/compare/1.0-rc1...1.0
Release Checklist
-----------------
## Release Checklist
1. Create release branch: `git flow release start 1.x.0`
2. Update CHANGELOG.md:
- 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=rc1`
4. Run: `goxc interpolate-source` to update VERSION var
5. Run tests
6. Test cross-compile: `goxc`
7. Commit changes and merge release: `git flow release finish 1.x.0`
8. Upload to bintray: `goxc bintray`
9. Update `binary_versions` option in `inbucket-site/_config.yml`
- Ensure *Unreleased* section is up to date
- Rename *Unreleased* section to release name and date.
- Add new GitHub `/compare` link
3. Run tests
4. Test cross-compile: `goreleaser --snapshot`
5. Commit changes and merge release: `git flow release finish`
6. Push tags and wait for https://travis-ci.org/jhillyerd/inbucket build to
complete
7. Update `binary_versions` option in `inbucket-site/_config.yml`
See http://keepachangelog.com/ for additional instructions on how to update this file.

35
Makefile Normal file
View File

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

View File

@@ -1,4 +1,5 @@
package smtpd
// Package datastore contains implementation independent datastore logic
package datastore
import (
"errors"

View File

@@ -1,4 +1,4 @@
package smtpd
package datastore
import (
"container/list"
@@ -36,6 +36,11 @@ func init() {
rm.Set("Period", expRetentionPeriod)
rm.Set("RetainedHist", expRetainedHist)
rm.Set("RetainedCurrent", expRetainedCurrent)
log.AddTickerFunc(func() {
expRetentionDeletesHist.Set(log.PushMetric(retentionDeletesHist, expRetentionDeletesTotal))
expRetainedHist.Set(log.PushMetric(retainedHist, expRetainedCurrent))
})
}
// RetentionScanner looks for messages older than the configured retention period and deletes them.
@@ -85,9 +90,9 @@ retentionLoop:
dur := time.Minute - since
log.Tracef("Retention scanner sleeping for %v", dur)
select {
case _ = <-rs.globalShutdown:
case <-rs.globalShutdown:
break retentionLoop
case _ = <-time.After(dur):
case <-time.After(dur):
}
}
// Kickoff scan
@@ -97,7 +102,7 @@ retentionLoop:
}
// Check for global shutdown
select {
case _ = <-rs.globalShutdown:
case <-rs.globalShutdown:
break retentionLoop
default:
}
@@ -154,9 +159,7 @@ func (rs *RetentionScanner) doScan() error {
// Join does not retun until the retention scanner has shut down
func (rs *RetentionScanner) Join() {
if rs.retentionShutdown != nil {
select {
case <-rs.retentionShutdown:
}
<-rs.retentionShutdown
}
}

View File

@@ -0,0 +1,67 @@
package datastore
import (
"fmt"
"testing"
"time"
)
func TestDoRetentionScan(t *testing.T) {
// Create mock objects
mds := &MockDataStore{}
mb1 := &MockMailbox{}
mb2 := &MockMailbox{}
mb3 := &MockMailbox{}
// Mockup some different aged messages (num is in hours)
new1 := mockMessage(0)
new2 := mockMessage(1)
new3 := mockMessage(2)
old1 := mockMessage(4)
old2 := mockMessage(12)
old3 := mockMessage(24)
// First it should ask for all mailboxes
mds.On("AllMailboxes").Return([]Mailbox{mb1, mb2, mb3}, nil)
// Then for all messages on each box
mb1.On("GetMessages").Return([]Message{new1, old1, old2}, nil)
mb2.On("GetMessages").Return([]Message{old3, new2}, nil)
mb3.On("GetMessages").Return([]Message{new3}, nil)
// Test 4 hour retention
rs := &RetentionScanner{
ds: mds,
retentionPeriod: 4*time.Hour - time.Minute,
retentionSleep: 0,
}
if err := rs.doScan(); err != nil {
t.Error(err)
}
// Check our assertions
mds.AssertExpectations(t)
mb1.AssertExpectations(t)
mb2.AssertExpectations(t)
mb3.AssertExpectations(t)
// Delete should not have been called on new messages
new1.AssertNotCalled(t, "Delete")
new2.AssertNotCalled(t, "Delete")
new3.AssertNotCalled(t, "Delete")
// Delete should have been called once on old messages
old1.AssertNumberOfCalls(t, "Delete", 1)
old2.AssertNumberOfCalls(t, "Delete", 1)
old3.AssertNumberOfCalls(t, "Delete", 1)
}
// Make a MockMessage of a specific age
func mockMessage(ageHours int) *MockMessage {
msg := &MockMessage{}
msg.On("ID").Return(fmt.Sprintf("MSG[age=%vh]", ageHours))
msg.On("Date").Return(time.Now().Add(time.Duration(ageHours*-1) * time.Hour))
msg.On("Delete").Return(nil)
return msg
}

View File

@@ -1,4 +1,4 @@
package rest
package datastore
import (
"io"
@@ -6,130 +6,151 @@ import (
"time"
"github.com/jhillyerd/enmime"
"github.com/jhillyerd/inbucket/smtpd"
"github.com/stretchr/testify/mock"
)
// Mock DataStore object
// MockDataStore is a shared mock for unit testing
type MockDataStore struct {
mock.Mock
}
func (m *MockDataStore) MailboxFor(name string) (smtpd.Mailbox, error) {
// MailboxFor mock function
func (m *MockDataStore) MailboxFor(name string) (Mailbox, error) {
args := m.Called(name)
return args.Get(0).(smtpd.Mailbox), args.Error(1)
return args.Get(0).(Mailbox), args.Error(1)
}
func (m *MockDataStore) AllMailboxes() ([]smtpd.Mailbox, error) {
// AllMailboxes mock function
func (m *MockDataStore) AllMailboxes() ([]Mailbox, error) {
args := m.Called()
return args.Get(0).([]smtpd.Mailbox), args.Error(1)
return args.Get(0).([]Mailbox), args.Error(1)
}
// Mock Mailbox object
// MockMailbox is a shared mock for unit testing
type MockMailbox struct {
mock.Mock
}
func (m *MockMailbox) GetMessages() ([]smtpd.Message, error) {
// GetMessages mock function
func (m *MockMailbox) GetMessages() ([]Message, error) {
args := m.Called()
return args.Get(0).([]smtpd.Message), args.Error(1)
return args.Get(0).([]Message), args.Error(1)
}
func (m *MockMailbox) GetMessage(id string) (smtpd.Message, error) {
// GetMessage mock function
func (m *MockMailbox) GetMessage(id string) (Message, error) {
args := m.Called(id)
return args.Get(0).(smtpd.Message), args.Error(1)
return args.Get(0).(Message), args.Error(1)
}
// Purge mock function
func (m *MockMailbox) Purge() error {
args := m.Called()
return args.Error(0)
}
func (m *MockMailbox) NewMessage() (smtpd.Message, error) {
// NewMessage mock function
func (m *MockMailbox) NewMessage() (Message, error) {
args := m.Called()
return args.Get(0).(smtpd.Message), args.Error(1)
return args.Get(0).(Message), args.Error(1)
}
// Name mock function
func (m *MockMailbox) Name() string {
args := m.Called()
return args.String(0)
}
// String mock function
func (m *MockMailbox) String() string {
args := m.Called()
return args.String(0)
}
// Mock Message object
// MockMessage is a shared mock for unit testing
type MockMessage struct {
mock.Mock
}
// ID mock function
func (m *MockMessage) ID() string {
args := m.Called()
return args.String(0)
}
// From mock function
func (m *MockMessage) From() string {
args := m.Called()
return args.String(0)
}
// To mock function
func (m *MockMessage) To() []string {
args := m.Called()
return args.Get(0).([]string)
}
// Date mock function
func (m *MockMessage) Date() time.Time {
args := m.Called()
return args.Get(0).(time.Time)
}
// Subject mock function
func (m *MockMessage) Subject() string {
args := m.Called()
return args.String(0)
}
// ReadHeader mock function
func (m *MockMessage) ReadHeader() (msg *mail.Message, err error) {
args := m.Called()
return args.Get(0).(*mail.Message), args.Error(1)
}
// ReadBody mock function
func (m *MockMessage) ReadBody() (body *enmime.Envelope, err error) {
args := m.Called()
return args.Get(0).(*enmime.Envelope), args.Error(1)
}
// ReadRaw mock function
func (m *MockMessage) ReadRaw() (raw *string, err error) {
args := m.Called()
return args.Get(0).(*string), args.Error(1)
}
// RawReader mock function
func (m *MockMessage) RawReader() (reader io.ReadCloser, err error) {
args := m.Called()
return args.Get(0).(io.ReadCloser), args.Error(1)
}
// Size mock function
func (m *MockMessage) Size() int64 {
args := m.Called()
return int64(args.Int(0))
}
// Append mock function
func (m *MockMessage) Append(data []byte) error {
// []byte arg seems to mess up testify/mock
return nil
}
// Close mock function
func (m *MockMessage) Close() error {
args := m.Called()
return args.Error(0)
}
// Delete mock function
func (m *MockMessage) Delete() error {
args := m.Called()
return args.Error(0)
}
// String mock function
func (m *MockMessage) String() string {
args := m.Called()
return args.String(0)

View File

@@ -1,4 +1,4 @@
package smtpd
package filestore
import (
"bufio"
@@ -11,6 +11,7 @@ import (
"time"
"github.com/jhillyerd/enmime"
"github.com/jhillyerd/inbucket/datastore"
"github.com/jhillyerd/inbucket/log"
)
@@ -33,7 +34,7 @@ type FileMessage struct {
// NewMessage creates a new FileMessage object and sets the Date and Id fields.
// It will also delete messages over messageCap if configured.
func (mb *FileMailbox) NewMessage() (Message, error) {
func (mb *FileMailbox) NewMessage() (datastore.Message, error) {
// Load index
if !mb.indexLoaded {
if err := mb.readIndex(); err != nil {
@@ -71,7 +72,7 @@ func (m *FileMessage) From() string {
return m.Ffrom
}
// From returns the value of the Message To header
// To returns the value of the Message To header
func (m *FileMessage) To() []string {
return m.Fto
}
@@ -165,7 +166,7 @@ func (m *FileMessage) ReadRaw() (raw *string, err error) {
func (m *FileMessage) Append(data []byte) error {
// Prevent Appending to a pre-existing Message
if !m.writable {
return ErrNotWritable
return datastore.ErrNotWritable
}
// Open file for writing if we haven't yet
if m.writer == nil {

View File

@@ -1,4 +1,4 @@
package smtpd
package filestore
import (
"bufio"
@@ -12,7 +12,9 @@ import (
"time"
"github.com/jhillyerd/inbucket/config"
"github.com/jhillyerd/inbucket/datastore"
"github.com/jhillyerd/inbucket/log"
"github.com/jhillyerd/inbucket/stringutil"
)
// Name of index file in each mailbox
@@ -55,7 +57,7 @@ type FileDataStore struct {
}
// NewFileDataStore creates a new DataStore object using the specified path
func NewFileDataStore(cfg config.DataStoreConfig) DataStore {
func NewFileDataStore(cfg config.DataStoreConfig) datastore.DataStore {
path := cfg.Path
if path == "" {
log.Errorf("No value configured for datastore path")
@@ -73,19 +75,19 @@ func NewFileDataStore(cfg config.DataStoreConfig) DataStore {
// DefaultFileDataStore creates a new DataStore object. It uses the inbucket.Config object to
// construct it's path.
func DefaultFileDataStore() DataStore {
func DefaultFileDataStore() datastore.DataStore {
cfg := config.GetDataStoreConfig()
return NewFileDataStore(cfg)
}
// MailboxFor retrieves the Mailbox object for a specified email address, if the mailbox
// does not exist, it will attempt to create it.
func (ds *FileDataStore) MailboxFor(emailAddress string) (Mailbox, error) {
name, err := ParseMailboxName(emailAddress)
func (ds *FileDataStore) MailboxFor(emailAddress string) (datastore.Mailbox, error) {
name, err := stringutil.ParseMailboxName(emailAddress)
if err != nil {
return nil, err
}
dir := HashMailboxName(name)
dir := stringutil.HashMailboxName(name)
s1 := dir[0:3]
s2 := dir[0:6]
path := filepath.Join(ds.mailPath, s1, s2, dir)
@@ -96,8 +98,8 @@ func (ds *FileDataStore) MailboxFor(emailAddress string) (Mailbox, error) {
}
// AllMailboxes returns a slice with all Mailboxes
func (ds *FileDataStore) AllMailboxes() ([]Mailbox, error) {
mailboxes := make([]Mailbox, 0, 100)
func (ds *FileDataStore) AllMailboxes() ([]datastore.Mailbox, error) {
mailboxes := make([]datastore.Mailbox, 0, 100)
infos1, err := ioutil.ReadDir(ds.mailPath)
if err != nil {
return nil, err
@@ -149,24 +151,26 @@ type FileMailbox struct {
messages []*FileMessage
}
// Name of the mailbox
func (mb *FileMailbox) Name() string {
return mb.name
}
// String renders the name and directory path of the mailbox
func (mb *FileMailbox) String() string {
return mb.name + "[" + mb.dirName + "]"
}
// GetMessages scans the mailbox directory for .gob files and decodes them into
// a slice of Message objects.
func (mb *FileMailbox) GetMessages() ([]Message, error) {
func (mb *FileMailbox) GetMessages() ([]datastore.Message, error) {
if !mb.indexLoaded {
if err := mb.readIndex(); err != nil {
return nil, err
}
}
messages := make([]Message, len(mb.messages))
messages := make([]datastore.Message, len(mb.messages))
for i, m := range mb.messages {
messages[i] = m
}
@@ -174,7 +178,7 @@ func (mb *FileMailbox) GetMessages() ([]Message, error) {
}
// GetMessage decodes a single message by Id and returns a Message object
func (mb *FileMailbox) GetMessage(id string) (Message, error) {
func (mb *FileMailbox) GetMessage(id string) (datastore.Message, error) {
if !mb.indexLoaded {
if err := mb.readIndex(); err != nil {
return nil, err
@@ -183,15 +187,15 @@ func (mb *FileMailbox) GetMessage(id string) (Message, error) {
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
}
}
for _, m := range mb.messages {
if m.Fid == id {
return m, nil
}
}
return nil, ErrNotExist
return nil, datastore.ErrNotExist
}
// Purge deletes all messages in this mailbox

View File

@@ -1,4 +1,4 @@
package smtpd
package filestore
import (
"bytes"
@@ -470,8 +470,8 @@ func TestGetLatestMessage(t *testing.T) {
mb, err := ds.MailboxFor(mbName)
assert.Nil(t, err)
msg, err := mb.GetMessage("latest")
assert.Nil(t, msg)
assert.Error(t, err)
fmt.Println(msg)
// Deliver test message
deliverMessage(ds, mbName, "test", time.Now())
@@ -496,7 +496,7 @@ func TestGetLatestMessage(t *testing.T) {
assert.True(t, msg.ID() == id3, "Expected %q to be equal to %q", msg.ID(), id3)
// Test wrong id
msg, err = mb.GetMessage("wrongid")
_, err = mb.GetMessage("wrongid")
assert.Error(t, err)
if t.Failed() {

View File

@@ -7,15 +7,15 @@ import (
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/jhillyerd/inbucket/config"
"github.com/jhillyerd/inbucket/datastore"
"github.com/jhillyerd/inbucket/msghub"
"github.com/jhillyerd/inbucket/smtpd"
)
// Context is passed into every request handler function
type Context struct {
Vars map[string]string
Session *sessions.Session
DataStore smtpd.DataStore
DataStore datastore.DataStore
MsgHub *msghub.Hub
WebConfig config.WebConfig
IsJSON bool

View File

@@ -13,9 +13,9 @@ import (
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
"github.com/jhillyerd/inbucket/config"
"github.com/jhillyerd/inbucket/datastore"
"github.com/jhillyerd/inbucket/log"
"github.com/jhillyerd/inbucket/msghub"
"github.com/jhillyerd/inbucket/smtpd"
)
// Handler is a function type that handles an HTTP request in Inbucket
@@ -23,7 +23,7 @@ type Handler func(http.ResponseWriter, *http.Request, *Context) error
var (
// DataStore is where all the mailboxes and messages live
DataStore smtpd.DataStore
DataStore datastore.DataStore
// msgHub holds a reference to the message pub/sub system
msgHub *msghub.Hub
@@ -51,7 +51,7 @@ func init() {
func Initialize(
cfg config.WebConfig,
shutdownChan chan bool,
ds smtpd.DataStore,
ds datastore.DataStore,
mh *msghub.Hub) {
webConfig = cfg

View File

@@ -13,6 +13,7 @@ import (
"time"
"github.com/jhillyerd/inbucket/config"
"github.com/jhillyerd/inbucket/filestore"
"github.com/jhillyerd/inbucket/httpd"
"github.com/jhillyerd/inbucket/log"
"github.com/jhillyerd/inbucket/msghub"
@@ -24,7 +25,7 @@ import (
var (
// version contains the build version number, populated during linking
version = "1.2.0"
version = "undefined"
// date contains the build date, populated during linking
date = "undefined"
@@ -85,7 +86,7 @@ func main() {
}
// Setup signal handler
sigChan := make(chan os.Signal)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
// Initialize logging
@@ -115,7 +116,7 @@ func main() {
msgHub := msghub.New(rootCtx, config.GetWebConfig().MonitorHistory)
// Grab our datastore
ds := smtpd.DefaultFileDataStore()
ds := filestore.DefaultFileDataStore()
// Start HTTP server
httpd.Initialize(config.GetWebConfig(), shutdownChan, ds, msgHub)
@@ -124,8 +125,7 @@ func main() {
go httpd.Start(rootCtx)
// Start POP3 server
// TODO pass datastore
pop3Server = pop3d.New(shutdownChan)
pop3Server = pop3d.New(config.GetPOP3Config(), shutdownChan, ds)
go pop3Server.Start(rootCtx)
// Startup SMTP server
@@ -150,7 +150,7 @@ signalLoop:
log.Infof("Received SIGTERM, shutting down")
close(shutdownChan)
}
case _ = <-shutdownChan:
case <-shutdownChan:
rootCancel()
break signalLoop
}

62
log/metrics.go Normal file
View File

@@ -0,0 +1,62 @@
package log
import (
"container/list"
"expvar"
"strings"
"time"
)
// TickerFunc is the type of metrics function accepted by AddTickerFunc
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
}
// PushMetric 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 PushMetric(history *list.List, ev expvar.Var) string {
history.PushBack(ev.String())
if history.Len() > 61 {
history.Remove(history.Front())
}
return joinStringList(history)
}
// 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, ",")
}
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)
}
}
}

View File

@@ -11,8 +11,8 @@ import (
"strings"
"time"
"github.com/jhillyerd/inbucket/datastore"
"github.com/jhillyerd/inbucket/log"
"github.com/jhillyerd/inbucket/smtpd"
)
// State tracks the current mode of our POP3 state machine
@@ -57,18 +57,18 @@ var commands = map[string]bool{
// Session defines an active POP3 session
type Session struct {
server *Server // Reference to the server we belong to
id int // Session ID number
conn net.Conn // Our network connection
remoteHost string // IP address of client
sendError error // Used to bail out of read loop on send error
state State // Current session state
reader *bufio.Reader // Buffered reader for our net conn
user string // Mailbox name
mailbox smtpd.Mailbox // Mailbox instance
messages []smtpd.Message // Slice of messages in mailbox
retain []bool // Messages to retain upon UPDATE (true=retain)
msgCount int // Number of undeleted messages
server *Server // Reference to the server we belong to
id int // Session ID number
conn net.Conn // Our network connection
remoteHost string // IP address of client
sendError error // Used to bail out of read loop on send error
state State // Current session state
reader *bufio.Reader // Buffered reader for our net conn
user string // Mailbox name
mailbox datastore.Mailbox // Mailbox instance
messages []datastore.Message // Slice of messages in mailbox
retain []bool // Messages to retain upon UPDATE (true=retain)
msgCount int // Number of undeleted messages
}
// NewSession creates a new POP3 session
@@ -432,7 +432,7 @@ func (ses *Session) transactionHandler(cmd string, args []string) {
}
// Send the contents of the message to the client
func (ses *Session) sendMessage(msg smtpd.Message) {
func (ses *Session) sendMessage(msg datastore.Message) {
reader, err := msg.RawReader()
if err != nil {
ses.logError("Failed to read message for RETR command")
@@ -465,7 +465,7 @@ func (ses *Session) sendMessage(msg smtpd.Message) {
}
// Send the headers plus the top N lines to the client
func (ses *Session) sendMessageTop(msg smtpd.Message, lineCount int) {
func (ses *Session) sendMessageTop(msg datastore.Message, lineCount int) {
reader, err := msg.RawReader()
if err != nil {
ses.logError("Failed to read message for RETR command")

View File

@@ -8,29 +8,25 @@ import (
"time"
"github.com/jhillyerd/inbucket/config"
"github.com/jhillyerd/inbucket/datastore"
"github.com/jhillyerd/inbucket/log"
"github.com/jhillyerd/inbucket/smtpd"
)
// Server defines an instance of our POP3 server
type Server struct {
host string
domain string
maxIdleSeconds int
dataStore smtpd.DataStore
dataStore datastore.DataStore
listener net.Listener
globalShutdown chan bool
waitgroup *sync.WaitGroup
}
// New creates a new Server struct
func New(shutdownChan chan bool) *Server {
// Get a new instance of the the FileDataStore - the locking and counting
// mechanisms are both global variables in the smtpd package. If that
// changes in the future, this should be modified to use the same DataStore
// instance.
ds := smtpd.DefaultFileDataStore()
cfg := config.GetPOP3Config()
func New(cfg config.POP3Config, shutdownChan chan bool, ds datastore.DataStore) *Server {
return &Server{
host: fmt.Sprintf("%v:%v", cfg.IP4address, cfg.IP4port),
domain: cfg.Domain,
dataStore: ds,
maxIdleSeconds: cfg.MaxIdleSeconds,
@@ -41,9 +37,7 @@ func New(shutdownChan chan bool) *Server {
// Start the server and listen for connections
func (s *Server) Start(ctx context.Context) {
cfg := config.GetPOP3Config()
addr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%v:%v",
cfg.IP4address, cfg.IP4port))
addr, err := net.ResolveTCPAddr("tcp4", s.host)
if err != nil {
log.Errorf("POP3 Failed to build tcp4 address: %v", err)
s.emergencyShutdown()

View File

@@ -10,16 +10,17 @@ import (
"io/ioutil"
"strconv"
"github.com/jhillyerd/inbucket/datastore"
"github.com/jhillyerd/inbucket/httpd"
"github.com/jhillyerd/inbucket/log"
"github.com/jhillyerd/inbucket/rest/model"
"github.com/jhillyerd/inbucket/smtpd"
"github.com/jhillyerd/inbucket/stringutil"
)
// MailboxListV1 renders a list of messages in a mailbox
func MailboxListV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}
@@ -54,7 +55,7 @@ func MailboxListV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context)
func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}
@@ -64,7 +65,7 @@ func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context)
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
msg, err := mb.GetMessage(id)
if err == smtpd.ErrNotExist {
if err == datastore.ErrNotExist {
http.NotFound(w, req)
return nil
}
@@ -116,7 +117,7 @@ func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context)
// MailboxPurgeV1 deletes all messages from a mailbox
func MailboxPurgeV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}
@@ -139,7 +140,7 @@ func MailboxPurgeV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context
func MailboxSourceV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}
@@ -149,7 +150,7 @@ func MailboxSourceV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Contex
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
message, err := mb.GetMessage(id)
if err == smtpd.ErrNotExist {
if err == datastore.ErrNotExist {
http.NotFound(w, req)
return nil
}
@@ -173,7 +174,7 @@ func MailboxSourceV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Contex
func MailboxDeleteV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}
@@ -183,7 +184,7 @@ func MailboxDeleteV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Contex
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
message, err := mb.GetMessage(id)
if err == smtpd.ErrNotExist {
if err == datastore.ErrNotExist {
http.NotFound(w, req)
return nil
}

View File

@@ -9,7 +9,7 @@ import (
"testing"
"time"
"github.com/jhillyerd/inbucket/smtpd"
"github.com/jhillyerd/inbucket/datastore"
)
const (
@@ -31,7 +31,7 @@ const (
func TestRestMailboxList(t *testing.T) {
// Setup
ds := &MockDataStore{}
ds := &datastore.MockDataStore{}
logbuf := setupWebServer(ds)
// Test invalid mailbox name
@@ -45,9 +45,9 @@ func TestRestMailboxList(t *testing.T) {
}
// Test empty mailbox
emptybox := &MockMailbox{}
emptybox := &datastore.MockMailbox{}
ds.On("MailboxFor", "empty").Return(emptybox, nil)
emptybox.On("GetMessages").Return([]smtpd.Message{}, nil)
emptybox.On("GetMessages").Return([]datastore.Message{}, nil)
w, err = testRestGet(baseURL + "/mailbox/empty")
expectCode = 200
@@ -59,7 +59,7 @@ func TestRestMailboxList(t *testing.T) {
}
// Test MailboxFor error
ds.On("MailboxFor", "error").Return(&MockMailbox{}, fmt.Errorf("Internal error"))
ds.On("MailboxFor", "error").Return(&datastore.MockMailbox{}, fmt.Errorf("Internal error"))
w, err = testRestGet(baseURL + "/mailbox/error")
expectCode = 500
if err != nil {
@@ -77,9 +77,9 @@ func TestRestMailboxList(t *testing.T) {
}
// Test MailboxFor error
error2box := &MockMailbox{}
error2box := &datastore.MockMailbox{}
ds.On("MailboxFor", "error2").Return(error2box, nil)
error2box.On("GetMessages").Return([]smtpd.Message{}, fmt.Errorf("Internal error 2"))
error2box.On("GetMessages").Return([]datastore.Message{}, fmt.Errorf("Internal error 2"))
w, err = testRestGet(baseURL + "/mailbox/error2")
expectCode = 500
@@ -107,11 +107,11 @@ func TestRestMailboxList(t *testing.T) {
Subject: "subject 2",
Date: time.Date(2012, 7, 1, 10, 11, 12, 253, time.FixedZone("PDT", -700)),
}
goodbox := &MockMailbox{}
goodbox := &datastore.MockMailbox{}
ds.On("MailboxFor", "good").Return(goodbox, nil)
msg1 := data1.MockMessage()
msg2 := data2.MockMessage()
goodbox.On("GetMessages").Return([]smtpd.Message{msg1, msg2}, nil)
goodbox.On("GetMessages").Return([]datastore.Message{msg1, msg2}, nil)
// Check return code
w, err = testRestGet(baseURL + "/mailbox/good")
@@ -155,7 +155,7 @@ func TestRestMailboxList(t *testing.T) {
func TestRestMessage(t *testing.T) {
// Setup
ds := &MockDataStore{}
ds := &datastore.MockDataStore{}
logbuf := setupWebServer(ds)
// Test invalid mailbox name
@@ -169,9 +169,9 @@ func TestRestMessage(t *testing.T) {
}
// Test requesting a message that does not exist
emptybox := &MockMailbox{}
emptybox := &datastore.MockMailbox{}
ds.On("MailboxFor", "empty").Return(emptybox, nil)
emptybox.On("GetMessage", "0001").Return(&MockMessage{}, smtpd.ErrNotExist)
emptybox.On("GetMessage", "0001").Return(&datastore.MockMessage{}, datastore.ErrNotExist)
w, err = testRestGet(baseURL + "/mailbox/empty/0001")
expectCode = 404
@@ -183,7 +183,7 @@ func TestRestMessage(t *testing.T) {
}
// Test MailboxFor error
ds.On("MailboxFor", "error").Return(&MockMailbox{}, fmt.Errorf("Internal error"))
ds.On("MailboxFor", "error").Return(&datastore.MockMailbox{}, fmt.Errorf("Internal error"))
w, err = testRestGet(baseURL + "/mailbox/error/0001")
expectCode = 500
if err != nil {
@@ -201,9 +201,9 @@ func TestRestMessage(t *testing.T) {
}
// Test GetMessage error
error2box := &MockMailbox{}
error2box := &datastore.MockMailbox{}
ds.On("MailboxFor", "error2").Return(error2box, nil)
error2box.On("GetMessage", "0001").Return(&MockMessage{}, fmt.Errorf("Internal error 2"))
error2box.On("GetMessage", "0001").Return(&datastore.MockMessage{}, fmt.Errorf("Internal error 2"))
w, err = testRestGet(baseURL + "/mailbox/error2/0001")
expectCode = 500
@@ -228,7 +228,7 @@ func TestRestMessage(t *testing.T) {
Text: "This is some text",
HTML: "This is some HTML",
}
goodbox := &MockMailbox{}
goodbox := &datastore.MockMailbox{}
ds.On("MailboxFor", "good").Return(goodbox, nil)
msg1 := data1.MockMessage()
goodbox.On("GetMessage", "0001").Return(msg1, nil)

View File

@@ -9,7 +9,7 @@ import (
"github.com/jhillyerd/inbucket/log"
"github.com/jhillyerd/inbucket/msghub"
"github.com/jhillyerd/inbucket/rest/model"
"github.com/jhillyerd/inbucket/smtpd"
"github.com/jhillyerd/inbucket/stringutil"
)
const (
@@ -169,7 +169,7 @@ func MonitorAllMessagesV1(
func MonitorMailboxMessagesV1(
w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) {
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}

View File

@@ -11,9 +11,9 @@ import (
"github.com/jhillyerd/enmime"
"github.com/jhillyerd/inbucket/config"
"github.com/jhillyerd/inbucket/datastore"
"github.com/jhillyerd/inbucket/httpd"
"github.com/jhillyerd/inbucket/msghub"
"github.com/jhillyerd/inbucket/smtpd"
)
type InputMessageData struct {
@@ -25,8 +25,8 @@ type InputMessageData struct {
HTML, Text string
}
func (d *InputMessageData) MockMessage() *MockMessage {
msg := &MockMessage{}
func (d *InputMessageData) MockMessage() *datastore.MockMessage {
msg := &datastore.MockMessage{}
msg.On("ID").Return(d.ID)
msg.On("From").Return(d.From)
msg.On("To").Return(d.To)
@@ -188,7 +188,7 @@ func testRestGet(url string) (*httptest.ResponseRecorder, error) {
return w, nil
}
func setupWebServer(ds smtpd.DataStore) *bytes.Buffer {
func setupWebServer(ds datastore.DataStore) *bytes.Buffer {
// Capture log output
buf := new(bytes.Buffer)
log.SetOutput(buf)

110
sanitize/css.go Normal file
View File

@@ -0,0 +1,110 @@
package sanitize
import (
"bytes"
"strings"
"github.com/gorilla/css/scanner"
)
// propertyRule may someday allow control of what values are valid for a particular property.
type propertyRule struct{}
var allowedProperties = map[string]propertyRule{
"align": {},
"background-color": {},
"border": {},
"border-bottom": {},
"border-left": {},
"border-radius": {},
"border-right": {},
"border-top": {},
"box-sizing": {},
"clear": {},
"color": {},
"content": {},
"display": {},
"font-family": {},
"font-size": {},
"font-weight": {},
"height": {},
"line-height": {},
"margin": {},
"margin-bottom": {},
"margin-left": {},
"margin-right": {},
"margin-top": {},
"max-height": {},
"max-width": {},
"overflow": {},
"padding": {},
"padding-bottom": {},
"padding-left": {},
"padding-right": {},
"padding-top": {},
"table-layout": {},
"text-align": {},
"text-decoration": {},
"text-shadow": {},
"vertical-align": {},
"width": {},
"word-break": {},
}
// Handler Token, return next state.
type stateHandler func(b *bytes.Buffer, t *scanner.Token) stateHandler
func sanitizeStyle(input string) string {
b := &bytes.Buffer{}
scan := scanner.New(input)
state := stateStart
for {
t := scan.Next()
if t.Type == scanner.TokenEOF {
return b.String()
}
if t.Type == scanner.TokenError {
return ""
}
state = state(b, t)
if state == nil {
return ""
}
}
}
func stateStart(b *bytes.Buffer, t *scanner.Token) stateHandler {
switch t.Type {
case scanner.TokenIdent:
_, ok := allowedProperties[strings.ToLower(t.Value)]
if !ok {
return stateEat
}
b.WriteString(t.Value)
return stateValid
case scanner.TokenS:
return stateStart
}
// Unexpected type.
b.WriteString("/*" + t.Type.String() + "*/")
return stateEat
}
func stateEat(b *bytes.Buffer, t *scanner.Token) stateHandler {
if t.Type == scanner.TokenChar && t.Value == ";" {
// Done eating.
return stateStart
}
// Throw away this token.
return stateEat
}
func stateValid(b *bytes.Buffer, t *scanner.Token) stateHandler {
state := stateValid
if t.Type == scanner.TokenChar && t.Value == ";" {
// End of property.
state = stateStart
}
b.WriteString(t.Value)
return state
}

34
sanitize/css_test.go Normal file
View File

@@ -0,0 +1,34 @@
package sanitize
import (
"testing"
)
func TestSanitizeStyle(t *testing.T) {
testCases := []struct {
input, want string
}{
{"", ""},
{
"color: red;",
"color: red;",
},
{
"background-color: black; color: white",
"background-color: black;color: white",
},
{
"background-color: black; invalid: true; color: white",
"background-color: black;color: white",
},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
got := sanitizeStyle(tc.input)
if got != tc.want {
t.Errorf("got: %q, want: %q, input: %q", got, tc.want, tc.input)
}
})
}
}

88
sanitize/html.go Normal file
View File

@@ -0,0 +1,88 @@
package sanitize
import (
"bufio"
"bytes"
"io"
"regexp"
"strings"
"github.com/microcosm-cc/bluemonday"
"golang.org/x/net/html"
)
var (
cssSafe = regexp.MustCompile(".*")
policy = bluemonday.UGCPolicy().
AllowElements("center").
AllowAttrs("style").Matching(cssSafe).Globally()
)
func HTML(html string) (output string, err error) {
output, err = sanitizeStyleTags(html)
if err != nil {
return "", err
}
output = policy.Sanitize(output)
return
}
func sanitizeStyleTags(input string) (string, error) {
r := strings.NewReader(input)
b := &bytes.Buffer{}
if err := styleTagFilter(b, r); err != nil {
return "", err
}
return b.String(), nil
}
func styleTagFilter(w io.Writer, r io.Reader) error {
bw := bufio.NewWriter(w)
b := make([]byte, 256)
z := html.NewTokenizer(r)
for {
b = b[:0]
tt := z.Next()
switch tt {
case html.ErrorToken:
err := z.Err()
if err == io.EOF {
return bw.Flush()
}
return err
case html.StartTagToken, html.SelfClosingTagToken:
name, hasAttr := z.TagName()
if !hasAttr {
bw.Write(z.Raw())
continue
}
b = append(b, '<')
b = append(b, name...)
for {
key, val, more := z.TagAttr()
strval := string(val)
style := false
if strings.ToLower(string(key)) == "style" {
style = true
strval = sanitizeStyle(strval)
}
if !style || strval != "" {
b = append(b, ' ')
b = append(b, key...)
b = append(b, '=', '"')
b = append(b, []byte(html.EscapeString(strval))...)
b = append(b, '"')
}
if !more {
break
}
}
if tt == html.SelfClosingTagToken {
b = append(b, '/')
}
bw.Write(append(b, '>'))
default:
bw.Write(z.Raw())
}
}
}

171
sanitize/html_test.go Normal file
View File

@@ -0,0 +1,171 @@
package sanitize_test
import (
"testing"
"github.com/jhillyerd/inbucket/sanitize"
)
// TestHTMLPlainStrings test plain text passthrough
func TestHTMLPlainStrings(t *testing.T) {
testStrings := []string{
"",
"plain string",
"one &lt; two",
}
for _, ts := range testStrings {
t.Run(ts, func(t *testing.T) {
got, err := sanitize.HTML(ts)
if err != nil {
t.Fatal(err)
}
if got != ts {
t.Errorf("Got: %q, want: %q", got, ts)
}
})
}
}
// TestHTMLSimpleFormatting tests basic tags we should allow
func TestHTMLSimpleFormatting(t *testing.T) {
testStrings := []string{
"<p>paragraph</p>",
"<b>bold</b>",
"<i>italic</b>",
"<em>emphasis</em>",
"<strong>strong</strong>",
"<div><span>text</span></div>",
"<center>text</center>",
}
for _, ts := range testStrings {
t.Run(ts, func(t *testing.T) {
got, err := sanitize.HTML(ts)
if err != nil {
t.Fatal(err)
}
if got != ts {
t.Errorf("Got: %q, want: %q", got, ts)
}
})
}
}
// TestHTMLScriptTags tests some strings with JavaScript
func TestHTMLScriptTags(t *testing.T) {
testCases := []struct {
input, want string
}{
{
`safe<script>nope</script>`,
`safe`,
},
{
`<a onblur="alert(something)" href="http://mysite.com">mysite</a>`,
`<a href="http://mysite.com" rel="nofollow">mysite</a>`,
},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
got, err := sanitize.HTML(tc.input)
if err != nil {
t.Fatal(err)
}
if got != tc.want {
t.Errorf("Got: %q, want: %q", got, tc.want)
}
})
}
}
func TestSanitizeStyleTags(t *testing.T) {
testCases := []struct {
name, input, want string
}{
{
"empty",
``,
``,
},
{
"open",
`<div>`,
`<div>`,
},
{
"open close",
`<div></div>`,
`<div></div>`,
},
{
"inner text",
`<div>foo bar</div>`,
`<div>foo bar</div>`,
},
{
"self close",
`<br/>`,
`<br/>`,
},
{
"open params",
`<div id="me">`,
`<div id="me">`,
},
{
"open params squote",
`<div id="me" title='best'>`,
`<div id="me" title="best">`,
},
{
"open style",
`<div id="me" style="color: red;">`,
`<div id="me" style="color: red;">`,
},
{
"open style squote",
`<div id="me" style='color: red;'>`,
`<div id="me" style="color: red;">`,
},
{
"open style mixed case",
`<div id="me" StYlE="color: red;">`,
`<div id="me" style="color: red;">`,
},
{
"closed style",
`<br style="border: 1px solid red;"/>`,
`<br style="border: 1px solid red;"/>`,
},
{
"mixed case style",
`<br StYlE="border: 1px solid red;"/>`,
`<br style="border: 1px solid red;"/>`,
},
{
"mixed case invalid style",
`<br StYlE="position: fixed;"/>`,
`<br/>`,
},
{
"mixed",
`<p id='i' title="cla'zz" style="font-size: 25px;"><b>some text</b></p>`,
`<p id="i" title="cla&#39;zz" style="font-size: 25px;"><b>some text</b></p>`,
},
{
"invalid styles",
`<div id="me" style='position: absolute;'>`,
`<div id="me">`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := sanitize.HTML(tc.input)
if err != nil {
t.Fatal(err)
}
if got != tc.want {
t.Errorf("input: %s\ngot : %s\nwant: %s", tc.input, got, tc.want)
}
})
}
}

View File

@@ -12,8 +12,10 @@ import (
"strings"
"time"
"github.com/jhillyerd/inbucket/datastore"
"github.com/jhillyerd/inbucket/log"
"github.com/jhillyerd/inbucket/msghub"
"github.com/jhillyerd/inbucket/stringutil"
)
// State tracks the current mode of our SMTP state machine
@@ -71,7 +73,7 @@ var commands = map[string]bool{
// recipientDetails for message delivery
type recipientDetails struct {
address, localPart, domainPart string
mailbox Mailbox
mailbox datastore.Mailbox
}
// Session holds the state of an SMTP session
@@ -265,7 +267,7 @@ func (ss *Session) readyHandler(cmd string, arg string) {
return
}
from := m[1]
if _, _, err := ParseEmailAddress(from); err != nil {
if _, _, err := stringutil.ParseEmailAddress(from); err != nil {
ss.send("501 Bad sender address syntax")
ss.logWarn("Bad address as MAIL arg: %q, %s", from, err)
return
@@ -314,7 +316,7 @@ func (ss *Session) mailHandler(cmd string, arg string) {
}
// This trim is probably too forgiving
recip := strings.Trim(arg[3:], "<> ")
if _, _, err := ParseEmailAddress(recip); err != nil {
if _, _, err := stringutil.ParseEmailAddress(recip); err != nil {
ss.send("501 Bad recipient address syntax")
ss.logWarn("Bad address as RCPT arg: %q, %s", recip, err)
return
@@ -354,7 +356,7 @@ func (ss *Session) dataHandler() {
if ss.server.storeMessages {
for e := ss.recipients.Front(); e != nil; e = e.Next() {
recip := e.Value.(string)
local, domain, err := ParseEmailAddress(recip)
local, domain, err := stringutil.ParseEmailAddress(recip)
if err != nil {
ss.logError("Failed to parse address for %q", recip)
ss.send(fmt.Sprintf("451 Failed to open mailbox for %v", recip))
@@ -510,20 +512,16 @@ func (ss *Session) send(msg string) {
// readByteLine reads a line of input into the provided buffer. Does
// not reset the Buffer - please do so prior to calling.
func (ss *Session) readByteLine(buf *bytes.Buffer) error {
func (ss *Session) readByteLine(buf io.Writer) error {
if err := ss.conn.SetReadDeadline(ss.nextDeadline()); err != nil {
return err
}
for {
line, err := ss.reader.ReadBytes('\n')
if err != nil {
return err
}
if _, err = buf.Write(line); err != nil {
return err
}
return nil
line, err := ss.reader.ReadBytes('\n')
if err != nil {
return err
}
_, err = buf.Write(line)
return err
}
// Reads a line of input
@@ -572,7 +570,7 @@ func (ss *Session) parseCmd(line string) (cmd string, arg string, ok bool) {
// The leading space is mandatory.
func (ss *Session) parseArgs(arg string) (args map[string]string, ok bool) {
args = make(map[string]string)
re := regexp.MustCompile(" (\\w+)=(\\w+)")
re := regexp.MustCompile(` (\w+)=(\w+)`)
pm := re.FindAllStringSubmatch(arg, -1)
if pm == nil {
ss.logWarn("Failed to parse arg string: %q")

View File

@@ -14,6 +14,7 @@ import (
"time"
"github.com/jhillyerd/inbucket/config"
"github.com/jhillyerd/inbucket/datastore"
"github.com/jhillyerd/inbucket/msghub"
)
@@ -25,17 +26,13 @@ type scriptStep struct {
// Test commands in GREET state
func TestGreetState(t *testing.T) {
// Setup mock objects
mds := &MockDataStore{}
mb1 := &MockMailbox{}
mds.On("MailboxFor").Return(mb1, nil)
mds := &datastore.MockDataStore{}
server, logbuf, teardown := setupSMTPServer(mds)
defer teardown()
var script []scriptStep
// Test out some mangled HELOs
script = []scriptStep{
script := []scriptStep{
{"HELO", 501},
{"EHLO", 501},
{"HELLO", 500},
@@ -86,17 +83,13 @@ func TestGreetState(t *testing.T) {
// Test commands in READY state
func TestReadyState(t *testing.T) {
// Setup mock objects
mds := &MockDataStore{}
mb1 := &MockMailbox{}
mds.On("MailboxFor").Return(mb1, nil)
mds := &datastore.MockDataStore{}
server, logbuf, teardown := setupSMTPServer(mds)
defer teardown()
var script []scriptStep
// Test out some mangled READY commands
script = []scriptStep{
script := []scriptStep{
{"HELO localhost", 250},
{"FOOB", 500},
{"HELO", 503},
@@ -151,10 +144,10 @@ func TestReadyState(t *testing.T) {
// Test commands in MAIL state
func TestMailState(t *testing.T) {
// Setup mock objects
mds := &MockDataStore{}
mb1 := &MockMailbox{}
msg1 := &MockMessage{}
mds.On("MailboxFor").Return(mb1, nil)
mds := &datastore.MockDataStore{}
mb1 := &datastore.MockMailbox{}
msg1 := &datastore.MockMessage{}
mds.On("MailboxFor", "u1").Return(mb1, nil)
mb1.On("NewMessage").Return(msg1, nil)
mb1.On("Name").Return("u1")
msg1.On("ID").Return("")
@@ -168,10 +161,8 @@ func TestMailState(t *testing.T) {
server, logbuf, teardown := setupSMTPServer(mds)
defer teardown()
var script []scriptStep
// Test out some mangled READY commands
script = []scriptStep{
script := []scriptStep{
{"HELO localhost", 250},
{"MAIL FROM:<john@gmail.com>", 250},
{"FOOB", 500},
@@ -268,10 +259,10 @@ func TestMailState(t *testing.T) {
// Test commands in DATA state
func TestDataState(t *testing.T) {
// Setup mock objects
mds := &MockDataStore{}
mb1 := &MockMailbox{}
msg1 := &MockMessage{}
mds.On("MailboxFor").Return(mb1, nil)
mds := &datastore.MockDataStore{}
mb1 := &datastore.MockMailbox{}
msg1 := &datastore.MockMessage{}
mds.On("MailboxFor", "u1").Return(mb1, nil)
mb1.On("NewMessage").Return(msg1, nil)
mb1.On("Name").Return("u1")
msg1.On("ID").Return("")
@@ -376,7 +367,7 @@ func (m *mockConn) SetDeadline(t time.Time) error { return nil }
func (m *mockConn) SetReadDeadline(t time.Time) error { return nil }
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
func setupSMTPServer(ds DataStore) (s *Server, buf *bytes.Buffer, teardown func()) {
func setupSMTPServer(ds datastore.DataStore) (s *Server, buf *bytes.Buffer, teardown func()) {
// Test Server Config
cfg := config.SMTPConfig{
IP4address: net.IPv4(127, 0, 0, 1),

View File

@@ -11,13 +11,35 @@ import (
"time"
"github.com/jhillyerd/inbucket/config"
"github.com/jhillyerd/inbucket/datastore"
"github.com/jhillyerd/inbucket/log"
"github.com/jhillyerd/inbucket/msghub"
)
func init() {
m := expvar.NewMap("smtp")
m.Set("ConnectsTotal", expConnectsTotal)
m.Set("ConnectsHist", expConnectsHist)
m.Set("ConnectsCurrent", expConnectsCurrent)
m.Set("ReceivedTotal", expReceivedTotal)
m.Set("ReceivedHist", expReceivedHist)
m.Set("ErrorsTotal", expErrorsTotal)
m.Set("ErrorsHist", expErrorsHist)
m.Set("WarnsTotal", expWarnsTotal)
m.Set("WarnsHist", expWarnsHist)
log.AddTickerFunc(func() {
expReceivedHist.Set(log.PushMetric(deliveredHist, expReceivedTotal))
expConnectsHist.Set(log.PushMetric(connectsHist, expConnectsTotal))
expErrorsHist.Set(log.PushMetric(errorsHist, expErrorsTotal))
expWarnsHist.Set(log.PushMetric(warnsHist, expWarnsTotal))
})
}
// Server holds the configuration and state of our SMTP server
type Server struct {
// Configuration
host string
domain string
domainNoStore string
maxRecips int
@@ -26,10 +48,10 @@ type Server struct {
storeMessages bool
// Dependencies
dataStore DataStore // Mailbox/message store
globalShutdown chan bool // Shuts down Inbucket
msgHub *msghub.Hub // Pub/sub for message info
retentionScanner *RetentionScanner // Deletes expired messages
dataStore datastore.DataStore // Mailbox/message store
globalShutdown chan bool // Shuts down Inbucket
msgHub *msghub.Hub // Pub/sub for message info
retentionScanner *datastore.RetentionScanner // Deletes expired messages
// State
listener net.Listener // Incoming network connections
@@ -61,9 +83,10 @@ var (
func NewServer(
cfg config.SMTPConfig,
globalShutdown chan bool,
ds DataStore,
ds datastore.DataStore,
msgHub *msghub.Hub) *Server {
return &Server{
host: fmt.Sprintf("%v:%v", cfg.IP4address, cfg.IP4port),
domain: cfg.Domain,
domainNoStore: strings.ToLower(cfg.DomainNoStore),
maxRecips: cfg.MaxRecipients,
@@ -73,16 +96,14 @@ func NewServer(
globalShutdown: globalShutdown,
dataStore: ds,
msgHub: msgHub,
retentionScanner: NewRetentionScanner(ds, globalShutdown),
retentionScanner: datastore.NewRetentionScanner(ds, globalShutdown),
waitgroup: new(sync.WaitGroup),
}
}
// Start the listener and handle incoming connections
func (s *Server) Start(ctx context.Context) {
cfg := config.GetSMTPConfig()
addr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%v:%v",
cfg.IP4address, cfg.IP4port))
addr, err := net.ResolveTCPAddr("tcp4", s.host)
if err != nil {
log.Errorf("Failed to build tcp4 address: %v", err)
s.emergencyShutdown()
@@ -110,10 +131,8 @@ func (s *Server) Start(ctx context.Context) {
go s.serve(ctx)
// Wait for shutdown
select {
case <-ctx.Done():
log.Tracef("SMTP shutdown requested, connections will be drained")
}
<-ctx.Done()
log.Tracef("SMTP shutdown requested, connections will be drained")
// Closing the listener will cause the serve() go routine to exit
if err := s.listener.Close(); err != nil {
@@ -165,7 +184,7 @@ func (s *Server) serve(ctx context.Context) {
func (s *Server) emergencyShutdown() {
// Shutdown Inbucket
select {
case _ = <-s.globalShutdown:
case <-s.globalShutdown:
default:
close(s.globalShutdown)
}
@@ -178,44 +197,3 @@ func (s *Server) Drain() {
log.Tracef("SMTP connections have drained")
s.retentionScanner.Join()
}
// When the provided Ticker ticks, we update our metrics history
func metricsTicker(t *time.Ticker) {
ok := true
for ok {
_, ok = <-t.C
expReceivedHist.Set(pushMetric(deliveredHist, expReceivedTotal))
expConnectsHist.Set(pushMetric(connectsHist, expConnectsTotal))
expErrorsHist.Set(pushMetric(errorsHist, expErrorsTotal))
expWarnsHist.Set(pushMetric(warnsHist, expWarnsTotal))
expRetentionDeletesHist.Set(pushMetric(retentionDeletesHist, expRetentionDeletesTotal))
expRetainedHist.Set(pushMetric(retainedHist, expRetainedCurrent))
}
}
// pushMetric 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 pushMetric(history *list.List, ev expvar.Var) string {
history.PushBack(ev.String())
if history.Len() > 61 {
history.Remove(history.Front())
}
return JoinStringList(history)
}
func init() {
m := expvar.NewMap("smtp")
m.Set("ConnectsTotal", expConnectsTotal)
m.Set("ConnectsHist", expConnectsHist)
m.Set("ConnectsCurrent", expConnectsCurrent)
m.Set("ReceivedTotal", expReceivedTotal)
m.Set("ReceivedHist", expReceivedHist)
m.Set("ErrorsTotal", expErrorsTotal)
m.Set("ErrorsHist", expErrorsHist)
m.Set("WarnsTotal", expWarnsTotal)
m.Set("WarnsHist", expWarnsHist)
t := time.NewTicker(time.Minute)
go metricsTicker(t)
}

View File

@@ -1,197 +0,0 @@
package smtpd
import (
"fmt"
"io"
"net/mail"
"testing"
"time"
"github.com/jhillyerd/enmime"
"github.com/stretchr/testify/mock"
)
func TestDoRetentionScan(t *testing.T) {
// Create mock objects
mds := &MockDataStore{}
mb1 := &MockMailbox{}
mb2 := &MockMailbox{}
mb3 := &MockMailbox{}
// Mockup some different aged messages (num is in hours)
new1 := mockMessage(0)
new2 := mockMessage(1)
new3 := mockMessage(2)
old1 := mockMessage(4)
old2 := mockMessage(12)
old3 := mockMessage(24)
// First it should ask for all mailboxes
mds.On("AllMailboxes").Return([]Mailbox{mb1, mb2, mb3}, nil)
// Then for all messages on each box
mb1.On("GetMessages").Return([]Message{new1, old1, old2}, nil)
mb2.On("GetMessages").Return([]Message{old3, new2}, nil)
mb3.On("GetMessages").Return([]Message{new3}, nil)
// Test 4 hour retention
rs := &RetentionScanner{
ds: mds,
retentionPeriod: 4*time.Hour - time.Minute,
retentionSleep: 0,
}
if err := rs.doScan(); err != nil {
t.Error(err)
}
// Check our assertions
mds.AssertExpectations(t)
mb1.AssertExpectations(t)
mb2.AssertExpectations(t)
mb3.AssertExpectations(t)
// Delete should not have been called on new messages
new1.AssertNotCalled(t, "Delete")
new2.AssertNotCalled(t, "Delete")
new3.AssertNotCalled(t, "Delete")
// Delete should have been called once on old messages
old1.AssertNumberOfCalls(t, "Delete", 1)
old2.AssertNumberOfCalls(t, "Delete", 1)
old3.AssertNumberOfCalls(t, "Delete", 1)
}
// Make a MockMessage of a specific age
func mockMessage(ageHours int) *MockMessage {
msg := &MockMessage{}
msg.On("ID").Return(fmt.Sprintf("MSG[age=%vh]", ageHours))
msg.On("Date").Return(time.Now().Add(time.Duration(ageHours*-1) * time.Hour))
msg.On("Delete").Return(nil)
return msg
}
// Mock DataStore object
type MockDataStore struct {
mock.Mock
}
func (m *MockDataStore) MailboxFor(name string) (Mailbox, error) {
args := m.Called()
return args.Get(0).(Mailbox), args.Error(1)
}
func (m *MockDataStore) AllMailboxes() ([]Mailbox, error) {
args := m.Called()
return args.Get(0).([]Mailbox), args.Error(1)
}
// Mock Mailbox object
type MockMailbox struct {
mock.Mock
}
func (m *MockMailbox) GetMessages() ([]Message, error) {
args := m.Called()
return args.Get(0).([]Message), args.Error(1)
}
func (m *MockMailbox) GetMessage(id string) (Message, error) {
args := m.Called(id)
return args.Get(0).(Message), args.Error(1)
}
func (m *MockMailbox) Purge() error {
args := m.Called()
return args.Error(0)
}
func (m *MockMailbox) NewMessage() (Message, error) {
args := m.Called()
return args.Get(0).(Message), args.Error(1)
}
func (m *MockMailbox) Name() string {
args := m.Called()
return args.String(0)
}
func (m *MockMailbox) String() string {
args := m.Called()
return args.String(0)
}
// Mock Message object
type MockMessage struct {
mock.Mock
}
func (m *MockMessage) ID() string {
args := m.Called()
return args.String(0)
}
func (m *MockMessage) From() string {
args := m.Called()
return args.String(0)
}
func (m *MockMessage) To() []string {
args := m.Called()
return args.Get(0).([]string)
}
func (m *MockMessage) Date() time.Time {
args := m.Called()
return args.Get(0).(time.Time)
}
func (m *MockMessage) Subject() string {
args := m.Called()
return args.String(0)
}
func (m *MockMessage) ReadHeader() (msg *mail.Message, err error) {
args := m.Called()
return args.Get(0).(*mail.Message), args.Error(1)
}
func (m *MockMessage) ReadBody() (body *enmime.Envelope, err error) {
args := m.Called()
return args.Get(0).(*enmime.Envelope), args.Error(1)
}
func (m *MockMessage) ReadRaw() (raw *string, err error) {
args := m.Called()
return args.Get(0).(*string), args.Error(1)
}
func (m *MockMessage) RawReader() (reader io.ReadCloser, err error) {
args := m.Called()
return args.Get(0).(io.ReadCloser), args.Error(1)
}
func (m *MockMessage) Size() int64 {
args := m.Called()
return int64(args.Int(0))
}
func (m *MockMessage) Append(data []byte) error {
// []byte arg seems to mess up testify/mock
return nil
}
func (m *MockMessage) Close() error {
args := m.Called()
return args.Error(0)
}
func (m *MockMessage) Delete() error {
args := m.Called()
return args.Error(0)
}
func (m *MockMessage) String() string {
args := m.Called()
return args.String(0)
}

View File

@@ -1,8 +1,7 @@
package smtpd
package stringutil
import (
"bytes"
"container/list"
"crypto/sha1"
"fmt"
"io"
@@ -42,7 +41,7 @@ func ParseMailboxName(localPart string) (result string, err error) {
return result, nil
}
// HashMailboxName accepts a mailbox name and hashes it. Inbucket uses this as
// HashMailboxName accepts a mailbox name and hashes it. filestore uses this as
// the directory to house the mailbox
func HashMailboxName(mailbox string) string {
h := sha1.New()
@@ -53,18 +52,6 @@ func HashMailboxName(mailbox string) string {
return fmt.Sprintf("%x", h.Sum(nil))
}
// 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, ",")
}
// ValidateDomainPart returns true if the domain part complies to RFC3696, RFC1035
func ValidateDomainPart(domain string) bool {
if len(domain) == 0 {
@@ -143,15 +130,24 @@ LOOP:
switch {
case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'):
// Letters are OK
_ = buf.WriteByte(c)
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
case '0' <= c && c <= '9':
// Numbers are OK
_ = buf.WriteByte(c)
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
case bytes.IndexByte([]byte("!#$%&'*+-/=?^_`{|}~"), c) >= 0:
// These specials can be used unquoted
_ = buf.WriteByte(c)
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
case c == '.':
// A single period is OK
@@ -159,13 +155,19 @@ LOOP:
// Sequence of periods is not permitted
return "", "", fmt.Errorf("Sequence of periods is not permitted")
}
_ = buf.WriteByte(c)
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
case c == '\\':
inCharQuote = true
case c == '"':
if inCharQuote {
_ = buf.WriteByte(c)
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
} else if inStringQuote {
inStringQuote = false
@@ -178,7 +180,10 @@ LOOP:
}
case c == '@':
if inCharQuote || inStringQuote {
_ = buf.WriteByte(c)
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
} else {
// End of local-part
@@ -195,7 +200,10 @@ LOOP:
return "", "", fmt.Errorf("Characters outside of US-ASCII range not permitted")
default:
if inCharQuote || inStringQuote {
_ = buf.WriteByte(c)
err = buf.WriteByte(c)
if err != nil {
return
}
inCharQuote = false
} else {
return "", "", fmt.Errorf("Character %q must be quoted", c)

View File

@@ -1,4 +1,4 @@
package smtpd
package stringutil
import (
"strings"

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
Subject: tutsplus responsive external CSS
MIME-Version: 1.0
Content-Type: text/html; charset="UTF-8"

View File

@@ -5,4 +5,6 @@ Subject: Swaks HTML
MIME-Version: 1.0
Content-Type: text/html; charset="UTF-8"
This is a test of <b>HTML</b> at the <i>top</i> level.
<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

@@ -53,5 +53,6 @@ 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

View File

@@ -51,12 +51,17 @@ body {
}
}
#body-tabs > li > a {
padding-top: 6px;
padding-bottom: 6px;
}
.message-body {
padding: 0 5px;
padding: 10px 4px;
}
.message-attachments {
margin-top: 20px;
margin-top: 5px;
padding: 10px 10px 0 0;
}

View File

@@ -22,6 +22,17 @@ function deleteMessage(id) {
})
}
// deleteMailbox clears the mailbox
function deleteMailbox() {
if (confirm("Are you sure you want delete this mailbox?")) {
$.ajax({
type: 'DELETE',
url: '/api/v1/mailbox/' + mailbox,
success: loadList
})
}
}
// flashTooltip temporarily changes the text of a tooltip
function flashTooltip(el, text) {
var prevText = $(el).attr('data-original-title');
@@ -43,7 +54,7 @@ function loadList() {
dataType: "json",
url: '/api/v1/mailbox/' + mailbox,
success: function(data) {
messageListData = data;
messageListData = data.reverse();
// Render list
$('#message-list').loadTemplate($('#list-entry-template'), data);
$('.message-list-entry').click(onMessageListClick);
@@ -144,6 +155,7 @@ function onMessageLoaded(responseText, textStatus, XMLHttpRequest) {
return;
}
onDocumentChange();
$('#body-tabs a:first').tab('show')
var top = $('#message-container').offset().top - navBarOffset;
$(window).scrollTop(top);
}

View File

@@ -30,7 +30,7 @@ function startMonitor(mailbox) {
$('#monitor-message-list').loadTemplate(
$('#message-template'),
msg,
{ append: true });
{ prepend: true });
});
ws.addEventListener('close', function (e) {
$('#conn-status').text('Disconnected!');

View File

@@ -24,7 +24,7 @@
class="btn btn-primary"
onClick="htmlView('{{.message.ID}}');">
<span class="glyphicon glyphicon-new-window" aria-hidden="true"></span>
HTML
Raw HTML
</button>
{{end}}
</div>
@@ -78,7 +78,22 @@
</div>
{{end}}
<div class="message-body">{{.body}}</div>
<nav>
<ul id="body-tabs" class="nav nav-tabs" role="tablist">
{{if .htmlAvailable}}
<li role="presentation">
<a href="#body-html" aria-controls="body-html" role="tab" data-toggle="tab">Safe HTML</a>
</li>
{{end}}
<li role="presentation">
<a href="#body-text" aria-controls="body-text" role="tab" data-toggle="tab">Plain Text</a>
</li>
</ul>
</nav>
<div class="tab-content">
<div role="tabpanel" class="tab-pane message-body" id="body-html">{{.htmlBody}}</div>
<div role="tabpanel" class="tab-pane message-body" id="body-text">{{.body}}</div>
</div>
{{with .attachments}}
<div class="well message-attachments">

View File

@@ -57,6 +57,14 @@ $(document).ready(function() {
onclick="loadList()">
<span class="glyphicon glyphicon-refresh" aria-hidden="true"></span>
</button>
<button class="btn btn-default"
type="button"
data-toggle="tooltip"
data-placement="top"
title="Delete&nbsp;Mailbox"
onclick="deleteMailbox()">
<span class="glyphicon glyphicon-trash"></span>
</button>
</div>
</div>
<div id="message-list-wrapper">

View File

@@ -7,9 +7,11 @@ import (
"net/http"
"strconv"
"github.com/jhillyerd/inbucket/datastore"
"github.com/jhillyerd/inbucket/httpd"
"github.com/jhillyerd/inbucket/log"
"github.com/jhillyerd/inbucket/smtpd"
"github.com/jhillyerd/inbucket/sanitize"
"github.com/jhillyerd/inbucket/stringutil"
)
// MailboxIndex renders the index page for a particular mailbox
@@ -23,7 +25,7 @@ func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *httpd.Context)
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
name, err = smtpd.ParseMailboxName(name)
name, err = stringutil.ParseMailboxName(name)
if err != nil {
ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w)
@@ -50,7 +52,7 @@ func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *httpd.Context)
func MailboxLink(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w)
@@ -66,7 +68,7 @@ func MailboxLink(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
// 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) {
// Don't have to validate these aren't empty, Gorilla returns 404
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}
@@ -93,7 +95,7 @@ func MailboxList(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}
@@ -103,7 +105,7 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
msg, err := mb.GetMessage(id)
if err == smtpd.ErrNotExist {
if err == datastore.ErrNotExist {
http.NotFound(w, req)
return nil
}
@@ -117,6 +119,14 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
}
body := template.HTML(httpd.TextToHTML(mime.Text))
htmlAvailable := mime.HTML != ""
var htmlBody template.HTML
if htmlAvailable {
if str, err := sanitize.HTML(mime.HTML); err == nil {
htmlBody = template.HTML(str)
} else {
log.Warnf("HTML sanitizer failed: %s", err)
}
}
// Render partial template
return httpd.RenderPartial("mailbox/_show.html", w, map[string]interface{}{
"ctx": ctx,
@@ -124,6 +134,7 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
"message": msg,
"body": body,
"htmlAvailable": htmlAvailable,
"htmlBody": htmlBody,
"mimeErrors": mime.Errors,
"attachments": mime.Attachments,
})
@@ -133,7 +144,7 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
func MailboxHTML(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}
@@ -143,7 +154,7 @@ func MailboxHTML(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
message, err := mb.GetMessage(id)
if err == smtpd.ErrNotExist {
if err == datastore.ErrNotExist {
http.NotFound(w, req)
return nil
}
@@ -170,7 +181,7 @@ func MailboxHTML(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
return err
}
@@ -180,7 +191,7 @@ func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *httpd.Context)
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
message, err := mb.GetMessage(id)
if err == smtpd.ErrNotExist {
if err == datastore.ErrNotExist {
http.NotFound(w, req)
return nil
}
@@ -205,7 +216,7 @@ func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *httpd.Context)
func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w)
@@ -226,7 +237,7 @@ func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *httpd.
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
message, err := mb.GetMessage(id)
if err == smtpd.ErrNotExist {
if err == datastore.ErrNotExist {
http.NotFound(w, req)
return nil
}
@@ -257,7 +268,7 @@ func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *httpd.
// MailboxViewAttach sends the attachment to the client for online viewing
func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w)
@@ -279,7 +290,7 @@ func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *httpd.Cont
return fmt.Errorf("Failed to get mailbox for %q: %v", name, err)
}
message, err := mb.GetMessage(id)
if err == smtpd.ErrNotExist {
if err == datastore.ErrNotExist {
http.NotFound(w, req)
return nil
}

View File

@@ -8,7 +8,7 @@ import (
"github.com/jhillyerd/inbucket/config"
"github.com/jhillyerd/inbucket/httpd"
"github.com/jhillyerd/inbucket/smtpd"
"github.com/jhillyerd/inbucket/stringutil"
)
// RootIndex serves the Inbucket landing page
@@ -58,7 +58,7 @@ func RootMonitorMailbox(w http.ResponseWriter, req *http.Request, ctx *httpd.Con
http.Redirect(w, req, httpd.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
name, err := stringutil.ParseMailboxName(ctx.Vars["name"])
if err != nil {
ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w)