mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f00b9ddef0 | ||
|
|
019e66d798 | ||
|
|
a3877e4f4b | ||
|
|
a89b6bbca2 | ||
|
|
f58e51d921 | ||
|
|
c39d5ded3f | ||
|
|
5f5a7eecd3 | ||
|
|
1ff8ffe9bd | ||
|
|
b4abdb6675 | ||
|
|
ffa756d895 | ||
|
|
d5aea4d635 | ||
|
|
3c19e0820b | ||
|
|
3b9af85924 | ||
|
|
26c38b1148 | ||
|
|
3062b70ea0 | ||
|
|
01d51302c4 | ||
|
|
dedd0eacff | ||
|
|
6431b71cfe | ||
|
|
25815767a7 | ||
|
|
06165cb3d3 | ||
|
|
ac21675bd7 | ||
|
|
f62eaa31b9 | ||
|
|
fcc0848bc0 | ||
|
|
dec67622ba | ||
|
|
11033a5359 | ||
|
|
3a4fd3f093 | ||
|
|
cc47895d71 | ||
|
|
76a77beca9 | ||
|
|
81eba8f51a | ||
|
|
c750dcff81 | ||
|
|
de75b778c0 | ||
|
|
0e72b414c4 | ||
|
|
52de1b2bfe | ||
|
|
b28e1d86d8 | ||
|
|
f4fadd7e44 | ||
|
|
28b40eb94d | ||
|
|
0f67e51e56 | ||
|
|
9d68e2c0a5 | ||
|
|
5bca2ae738 | ||
|
|
10cce5c751 | ||
|
|
8040b07c28 | ||
|
|
4e8c287608 | ||
|
|
6f57c51934 | ||
|
|
a457b65603 | ||
|
|
890d8e0202 | ||
|
|
9f6dee640e | ||
|
|
095796c8a1 | ||
|
|
db358fea8c |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -28,9 +28,6 @@ _testmain.go
|
||||
# our binaries
|
||||
/inbucket
|
||||
/inbucket.exe
|
||||
/target/**
|
||||
/dist/**
|
||||
/cmd/client/client
|
||||
/cmd/client/client.exe
|
||||
|
||||
# local goxc config
|
||||
.goxc.local.json
|
||||
|
||||
60
.goreleaser.yml
Normal file
60
.goreleaser.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
project_name: inbucket
|
||||
release:
|
||||
github:
|
||||
owner: jhillyerd
|
||||
name: inbucket
|
||||
name_template: '{{.Tag}}'
|
||||
brew:
|
||||
commit_author:
|
||||
name: goreleaserbot
|
||||
email: goreleaser@carlosbecker.com
|
||||
install: bin.install ""
|
||||
builds:
|
||||
- binary: inbucket
|
||||
goos:
|
||||
- darwin
|
||||
- freebsd
|
||||
- linux
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
goarm:
|
||||
- "6"
|
||||
main: .
|
||||
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
|
||||
- binary: client
|
||||
goos:
|
||||
- darwin
|
||||
- freebsd
|
||||
- linux
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
goarm:
|
||||
- "6"
|
||||
main: ./cmd/client
|
||||
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
|
||||
archive:
|
||||
format: tar.gz
|
||||
wrap_in_directory: true
|
||||
name_template: '{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{
|
||||
.Arm }}{{ end }}'
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- LICENSE*
|
||||
- README*
|
||||
- CHANGELOG*
|
||||
- inbucket.bat
|
||||
- etc/**/*
|
||||
- themes/**/*
|
||||
fpm:
|
||||
bindir: /usr/local/bin
|
||||
snapshot:
|
||||
name_template: SNAPSHOT-{{ .Commit }}
|
||||
checksum:
|
||||
name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt'
|
||||
dist: dist
|
||||
sign:
|
||||
artifacts: none
|
||||
18
.goxc.json
18
.goxc.json
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"ArtifactsDest": "target",
|
||||
"TasksExclude": [
|
||||
"pkg-build"
|
||||
],
|
||||
"Arch": "amd64",
|
||||
"Os": "darwin freebsd linux windows",
|
||||
"ResourcesInclude": "README*,LICENSE*,CHANGELOG*,inbucket.bat,etc,themes",
|
||||
"PackageVersion": "1.2.0",
|
||||
"PrereleaseInfo": "rc2",
|
||||
"ConfigVersion": "0.9",
|
||||
"BuildSettings": {
|
||||
"LdFlagsXVars": {
|
||||
"TimeNow": "main.BUILDDATE",
|
||||
"Version": "main.VERSION"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
.travis.yml
16
.travis.yml
@@ -1,11 +1,19 @@
|
||||
language: go
|
||||
sudo: false
|
||||
|
||||
env:
|
||||
- DEPLOY_WITH_MAJOR="1.9"
|
||||
|
||||
before_script:
|
||||
- go vet ./...
|
||||
- go get github.com/golang/lint/golint
|
||||
|
||||
go:
|
||||
- 1.8.5
|
||||
- 1.9.2
|
||||
- 1.9.x
|
||||
- "1.10"
|
||||
|
||||
script: go test -race -v ./...
|
||||
deploy:
|
||||
provider: script
|
||||
script: etc/travis-deploy.sh
|
||||
on:
|
||||
tags: true
|
||||
branch: master
|
||||
|
||||
83
CHANGELOG.md
83
CHANGELOG.md
@@ -4,8 +4,28 @@ 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-rc2] - 2017-12-15
|
||||
------------------------
|
||||
## [v1.3.1] - 2018-03-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Adding additional locking during message delivery to prevent race condition
|
||||
that could lose messages.
|
||||
|
||||
|
||||
## [v1.3.0] - 2018-02-28
|
||||
|
||||
### Added
|
||||
- Button to purge mailbox contents from the UI.
|
||||
- Simple HTML/CSS sanitization; `Safe HTML` and `Plain Text` UI tabs.
|
||||
|
||||
### Changed
|
||||
- Reverse message display sort order in the UI; now newest first.
|
||||
|
||||
|
||||
## [v1.2.0] - 2017-12-27
|
||||
|
||||
### Changed
|
||||
- No significant code changes from rc2
|
||||
|
||||
### Added
|
||||
- `rest/client` types `MessageHeader` and `Message` with convenience methods;
|
||||
@@ -20,8 +40,8 @@ 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)
|
||||
@@ -47,8 +67,8 @@ 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)
|
||||
@@ -56,8 +76,8 @@ 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
|
||||
@@ -67,8 +87,8 @@ 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
|
||||
@@ -82,8 +102,8 @@ 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
|
||||
@@ -91,29 +111,30 @@ 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-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.1]: https://github.com/jhillyerd/inbucket/compare/v1.3.0...v1.3.1
|
||||
[v1.3.0]: https://github.com/jhillyerd/inbucket/compare/v1.2.0...v1.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
35
Makefile
Normal 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}
|
||||
@@ -1,9 +1,11 @@
|
||||
package smtpd
|
||||
// Package datastore contains implementation independent datastore logic
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/mail"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jhillyerd/enmime"
|
||||
@@ -21,6 +23,8 @@ var (
|
||||
type DataStore interface {
|
||||
MailboxFor(emailAddress string) (Mailbox, error)
|
||||
AllMailboxes() ([]Mailbox, error)
|
||||
// LockFor is a temporary hack to fix #77 until Datastore revamp
|
||||
LockFor(emailAddress string) (*sync.RWMutex, error)
|
||||
}
|
||||
|
||||
// Mailbox is an interface to get and manipulate messages in a DataStore
|
||||
19
datastore/lock.go
Normal file
19
datastore/lock.go
Normal file
@@ -0,0 +1,19 @@
|
||||
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]
|
||||
}
|
||||
61
datastore/lock_test.go
Normal file
61
datastore/lock_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package datastore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jhillyerd/inbucket/datastore"
|
||||
)
|
||||
|
||||
func TestHashLock(t *testing.T) {
|
||||
hl := &datastore.HashLock{}
|
||||
|
||||
// Invalid hashes
|
||||
testCases := []struct {
|
||||
name, input string
|
||||
}{
|
||||
{"empty", ""},
|
||||
{"short", "a0"},
|
||||
{"badhex", "zzzzzzzzzzzzzzzzzzzzzzz"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
l := hl.Get(tc.input)
|
||||
if l != nil {
|
||||
t.Errorf("Expected nil lock for %s %q, got %v", tc.name, tc.input, l)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Valid hashes
|
||||
testStrings := []string{
|
||||
"deadbeef",
|
||||
"00000000",
|
||||
"ffffffff",
|
||||
}
|
||||
for _, ts := range testStrings {
|
||||
t.Run(ts, func(t *testing.T) {
|
||||
l := hl.Get(ts)
|
||||
if l == nil {
|
||||
t.Errorf("Expeced non-nil lock for hex string %q", ts)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
a := hl.Get("deadbeef")
|
||||
b := hl.Get("deadbeef")
|
||||
if a != b {
|
||||
t.Errorf("Expected identical locks for identical hashes, got: %p != %p", a, b)
|
||||
}
|
||||
|
||||
a = hl.Get("deadbeef")
|
||||
b = hl.Get("d3adb33f")
|
||||
if a == b {
|
||||
t.Errorf("Expected different locks for different hashes, got: %p == %p", a, b)
|
||||
}
|
||||
|
||||
a = hl.Get("deadbeef")
|
||||
b = hl.Get("deadb33f")
|
||||
if a != b {
|
||||
t.Errorf("Expected identical locks for identical leading hashes, got: %p != %p", a, b)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
67
datastore/retention_test.go
Normal file
67
datastore/retention_test.go
Normal 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
|
||||
}
|
||||
@@ -1,135 +1,161 @@
|
||||
package rest
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/mail"
|
||||
"sync"
|
||||
"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
|
||||
func (m *MockDataStore) LockFor(name string) (*sync.RWMutex, error) {
|
||||
return &sync.RWMutex{}, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -15,11 +15,14 @@ apk add --no-cache --virtual .build-deps git
|
||||
|
||||
# Setup
|
||||
export GOBIN="$bindir"
|
||||
builddate="$(date -Iseconds)"
|
||||
cd "$srcdir"
|
||||
go clean
|
||||
# Fetch tags for describe
|
||||
git fetch -t
|
||||
builddate="$(date -Iseconds)"
|
||||
buildver="$(git describe --tags --always)"
|
||||
|
||||
# Build
|
||||
go clean
|
||||
echo "### Fetching Dependencies"
|
||||
go get -t -v ./...
|
||||
|
||||
@@ -27,7 +30,7 @@ echo "### Testing Inbucket"
|
||||
go test ./...
|
||||
|
||||
echo "### Building Inbucket"
|
||||
go build -o inbucket -ldflags "-X 'main.BUILDDATE=$builddate'" -v .
|
||||
go build -o inbucket -ldflags "-X 'main.version=$buildver' -X 'main.date=$builddate'" -v .
|
||||
|
||||
echo "### Installing Inbucket"
|
||||
set -x
|
||||
|
||||
10
etc/travis-deploy.sh
Executable file
10
etc/travis-deploy.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
# travis-deploy.sh
|
||||
# description: Trigger goreleaser deployment in correct build scenarios
|
||||
|
||||
set -eo pipefail
|
||||
set -x
|
||||
|
||||
if [[ "$TRAVIS_GO_VERSION" == "$DEPLOY_WITH_MAJOR."* ]]; then
|
||||
curl -sL https://git.io/goreleaser | bash
|
||||
fi
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -49,13 +51,14 @@ func countGenerator(c chan int) {
|
||||
// FileDataStore implements DataStore aand is the root of the mail storage
|
||||
// hiearchy. It provides access to Mailbox objects
|
||||
type FileDataStore struct {
|
||||
hashLock datastore.HashLock
|
||||
path string
|
||||
mailPath string
|
||||
messageCap int
|
||||
}
|
||||
|
||||
// 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 +76,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 +99,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
|
||||
@@ -137,6 +140,15 @@ func (ds *FileDataStore) AllMailboxes() ([]Mailbox, error) {
|
||||
return mailboxes, nil
|
||||
}
|
||||
|
||||
func (ds *FileDataStore) LockFor(emailAddress string) (*sync.RWMutex, error) {
|
||||
name, err := stringutil.ParseMailboxName(emailAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hash := stringutil.HashMailboxName(name)
|
||||
return ds.hashLock.Get(hash), nil
|
||||
}
|
||||
|
||||
// FileMailbox implements Mailbox, manages the mail for a specific user and
|
||||
// correlates to a particular directory on disk.
|
||||
type FileMailbox struct {
|
||||
@@ -149,24 +161,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 +188,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 +197,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
|
||||
@@ -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() {
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
22
inbucket.go
22
inbucket.go
@@ -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"
|
||||
@@ -23,11 +24,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// VERSION contains the build version number, populated during linking by goxc
|
||||
VERSION = "1.2.0-rc2"
|
||||
// version contains the build version number, populated during linking
|
||||
version = "undefined"
|
||||
|
||||
// BUILDDATE contains the build date, populated during linking by goxc
|
||||
BUILDDATE = "undefined"
|
||||
// date contains the build date, populated during linking
|
||||
date = "undefined"
|
||||
|
||||
// Command line flags
|
||||
help = flag.Bool("help", false, "Displays this help")
|
||||
@@ -61,8 +62,8 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.Version = VERSION
|
||||
config.BuildDate = BUILDDATE
|
||||
config.Version = version
|
||||
config.BuildDate = date
|
||||
|
||||
flag.Parse()
|
||||
if *help {
|
||||
@@ -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
62
log/metrics.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
110
sanitize/css.go
Normal 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
34
sanitize/css_test.go
Normal 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
88
sanitize/html.go
Normal 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
171
sanitize/html_test.go
Normal 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 < 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'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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
@@ -400,7 +402,19 @@ func (ss *Session) dataHandler() {
|
||||
if ss.server.storeMessages {
|
||||
// Create a message for each valid recipient
|
||||
for _, r := range recipients {
|
||||
if ok := ss.deliverMessage(r, msgBuf); ok {
|
||||
// TODO temporary hack to fix #77 until datastore revamp
|
||||
mu, err := ss.server.dataStore.LockFor(r.localPart)
|
||||
if err != nil {
|
||||
ss.logError("Failed to get lock for %q: %s", r.localPart, err)
|
||||
// Delivery failure
|
||||
ss.send(fmt.Sprintf("451 Failed to store message for %v", r.localPart))
|
||||
ss.reset()
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
ok := ss.deliverMessage(r, msgBuf)
|
||||
mu.Unlock()
|
||||
if ok {
|
||||
expReceivedTotal.Add(1)
|
||||
} else {
|
||||
// Delivery failure
|
||||
@@ -510,20 +524,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 +582,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")
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
@@ -1,4 +1,4 @@
|
||||
package smtpd
|
||||
package stringutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
393
swaks-tests/nonmime-html-inlined.raw
Normal file
393
swaks-tests/nonmime-html-inlined.raw
Normal 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 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 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>
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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!');
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 Mailbox"
|
||||
onclick="deleteMailbox()">
|
||||
<span class="glyphicon glyphicon-trash"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="message-list-wrapper">
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user