mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13868d85d4 | ||
|
|
8f10e18fef | ||
|
|
b105bbf87f | ||
|
|
ad85a1db93 | ||
|
|
d418f4ba29 | ||
|
|
d98e6a2b58 | ||
|
|
8b7fbfda6a | ||
|
|
46fa714cc7 | ||
|
|
414ed44882 | ||
|
|
2e1c937d23 | ||
|
|
df11575b3a |
@@ -8,6 +8,6 @@
|
||||
"Include": "README*,LICENSE*,inbucket.bat,etc,themes"
|
||||
},
|
||||
"PackageVersion": "1.0",
|
||||
"PrereleaseInfo": "rc2",
|
||||
"PrereleaseInfo": "rc4",
|
||||
"FormatVersion": "0.8"
|
||||
}
|
||||
|
||||
34
README.md
34
README.md
@@ -2,34 +2,24 @@ Inbucket [.
|
||||
|
||||
It allows web developers, software engineers and system administrators to
|
||||
quickly see the emailed output of ther applications. No per-account setup is
|
||||
required! Mailboxes are created on the fly as mail is received for them, and
|
||||
no password is required to browse the content of the mailboxes.
|
||||
|
||||
Inbucket has a built-in SMTP server and stores incoming mail as flat files on
|
||||
disk - no external SMTP or database daemons required.
|
||||
|
||||
There is also an embedded POP3 server, which allows message rendering to be
|
||||
checked in multiple email programs or to verify message delivery as part of
|
||||
an integration test suite.
|
||||
|
||||
Read more at the [Inbucket website](http://jhillyerd.github.io/inbucket/).
|
||||
Read more at the [Inbucket website][Inbucket]
|
||||
|
||||
Development Status
|
||||
------------------
|
||||
|
||||
Inbucket is currently release-candidate quality: it is being used for real work.
|
||||
|
||||
Please check the [issues list](https://github.com/jhillyerd/inbucket/issues?state=open)
|
||||
Please check the [issues list][Issues]
|
||||
for more details.
|
||||
|
||||
Installation from Source
|
||||
Building from Source
|
||||
------------------------
|
||||
|
||||
You will need a functioning [Go installation][1] for this to work.
|
||||
You will need a functioning [Go installation][Golang] for this to work.
|
||||
|
||||
Grab the Inbucket source code and compile the daemon:
|
||||
|
||||
@@ -43,14 +33,18 @@ Unix and OS X machines as is. Launch the daemon:
|
||||
By default the SMTP server will be listening on localhost port 2500 and
|
||||
the web interface will be available at [localhost:9000](http://localhost:9000/).
|
||||
|
||||
There are RedHat EL6 init, logrotate and httpd proxy configs provided.
|
||||
The Inbucket website has a more complete guide to
|
||||
[installing from source][From Source]
|
||||
|
||||
About
|
||||
-----
|
||||
|
||||
Inbucket is written in [Google Go][1].
|
||||
Inbucket is written in [Google Go][Golang].
|
||||
|
||||
Inbucket is open source software released under the MIT License. The latest
|
||||
version can be found at https://github.com/jhillyerd/inbucket
|
||||
|
||||
[1]: http://golang.org/
|
||||
[Inbucket]: http://inbucket.org/
|
||||
[Issues]: https://github.com/jhillyerd/inbucket/issues?state=open
|
||||
[From Source]: http://inbucket.org/installation/from-source.html
|
||||
[Golang]: http://golang.org/
|
||||
|
||||
@@ -42,9 +42,14 @@ type DataStoreConfig struct {
|
||||
Path string
|
||||
RetentionMinutes int
|
||||
RetentionSleep int
|
||||
MailboxMsgCap int
|
||||
}
|
||||
|
||||
var (
|
||||
// Build info, set by main
|
||||
VERSION = ""
|
||||
BUILD_DATE = ""
|
||||
|
||||
// Global goconfig object
|
||||
Config *config.Config
|
||||
|
||||
@@ -121,6 +126,7 @@ func LoadConfig(filename string) error {
|
||||
requireOption(messages, "datastore", "path")
|
||||
requireOption(messages, "datastore", "retention.minutes")
|
||||
requireOption(messages, "datastore", "retention.sleep.millis")
|
||||
requireOption(messages, "datastore", "mailbox.message.cap")
|
||||
|
||||
// Return error if validations failed
|
||||
if messages.Len() > 0 {
|
||||
@@ -361,6 +367,11 @@ func parseDataStoreConfig() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
|
||||
}
|
||||
option = "mailbox.message.cap"
|
||||
dataStoreConfig.MailboxMsgCap, err = Config.Int(section, option)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -100,3 +100,8 @@ retention.minutes=0
|
||||
# This should help reduce disk I/O when there are a large number of messages
|
||||
# to purge.
|
||||
retention.sleep.millis=100
|
||||
|
||||
# Maximum number of messages we will store in a single mailbox. If this
|
||||
# number is exceeded, the oldest message in the box will be deleted each
|
||||
# time a new message is received for it.
|
||||
mailbox.message.cap=100
|
||||
|
||||
@@ -100,3 +100,13 @@ retention.minutes=240
|
||||
# This should help reduce disk I/O when there are a large number of messages
|
||||
# to purge.
|
||||
retention.sleep.millis=100
|
||||
|
||||
# Maximum number of messages we will store in a single mailbox. If this
|
||||
# number is exceeded, the oldest message in the box will be deleted each
|
||||
# time a new message is received for it.
|
||||
mailbox.message.cap=100
|
||||
|
||||
# Maximum number of messages we will store in a single mailbox. If this
|
||||
# number is exceeded, the oldest message in the box will be deleted each
|
||||
# time a new message is received for it.
|
||||
mailbox.message.cap=500
|
||||
|
||||
@@ -1 +1 @@
|
||||
curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/$1
|
||||
curl -i -H "Accept: application/json" --noproxy localhost "http://localhost:9000/mailbox/$1"
|
||||
|
||||
@@ -100,3 +100,8 @@ retention.minutes=240
|
||||
# This should help reduce disk I/O when there are a large number of messages
|
||||
# to purge.
|
||||
retention.sleep.millis=100
|
||||
|
||||
# Maximum number of messages we will store in a single mailbox. If this
|
||||
# number is exceeded, the oldest message in the box will be deleted each
|
||||
# time a new message is received for it.
|
||||
mailbox.message.cap=500
|
||||
|
||||
@@ -100,3 +100,8 @@ retention.minutes=240
|
||||
# This should help reduce disk I/O when there are a large number of messages
|
||||
# to purge.
|
||||
retention.sleep.millis=100
|
||||
|
||||
# Maximum number of messages we will store in a single mailbox. If this
|
||||
# number is exceeded, the oldest message in the box will be deleted each
|
||||
# time a new message is received for it.
|
||||
mailbox.message.cap=500
|
||||
|
||||
33
inbucket.go
33
inbucket.go
@@ -19,21 +19,31 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Command line flags
|
||||
var help = flag.Bool("help", false, "Displays this help")
|
||||
var pidfile = flag.String("pidfile", "none", "Write our PID into the specified file")
|
||||
var logfile = flag.String("logfile", "stderr", "Write out log into the specified file")
|
||||
var (
|
||||
// Build info, populated during linking by goxc
|
||||
VERSION = "1.0"
|
||||
BUILD_DATE = "undefined"
|
||||
|
||||
// startTime is used to calculate uptime of Inbucket
|
||||
var startTime = time.Now()
|
||||
// Command line flags
|
||||
help = flag.Bool("help", false, "Displays this help")
|
||||
pidfile = flag.String("pidfile", "none", "Write our PID into the specified file")
|
||||
logfile = flag.String("logfile", "stderr", "Write out log into the specified file")
|
||||
|
||||
// The file we send log output to, will be nil for stderr or stdout
|
||||
var logf *os.File
|
||||
// startTime is used to calculate uptime of Inbucket
|
||||
startTime = time.Now()
|
||||
|
||||
var smtpServer *smtpd.Server
|
||||
var pop3Server *pop3d.Server
|
||||
// The file we send log output to, will be nil for stderr or stdout
|
||||
logf *os.File
|
||||
|
||||
// Server instances
|
||||
smtpServer *smtpd.Server
|
||||
pop3Server *pop3d.Server
|
||||
)
|
||||
|
||||
func main() {
|
||||
config.VERSION = VERSION
|
||||
config.BUILD_DATE = BUILD_DATE
|
||||
|
||||
flag.Parse()
|
||||
if *help {
|
||||
flag.Usage()
|
||||
@@ -82,6 +92,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
log.LogInfo("Inbucket %v (%v) starting...", config.VERSION, config.BUILD_DATE)
|
||||
|
||||
// Write pidfile if requested
|
||||
// TODO: Probably supposed to remove pidfile during shutdown
|
||||
if *pidfile != "none" {
|
||||
@@ -98,6 +110,7 @@ func main() {
|
||||
ds := smtpd.DefaultFileDataStore()
|
||||
|
||||
// Start HTTP server
|
||||
web.Initialize(config.GetWebConfig(), ds)
|
||||
go web.Start()
|
||||
|
||||
// Start POP3 server
|
||||
|
||||
@@ -425,12 +425,12 @@ func (ses *Session) transactionHandler(cmd string, args []string) {
|
||||
// Send the contents of the message to the client
|
||||
func (ses *Session) sendMessage(msg smtpd.Message) {
|
||||
reader, err := msg.RawReader()
|
||||
defer reader.Close()
|
||||
if err != nil {
|
||||
ses.logError("Failed to read message for RETR command")
|
||||
ses.send("-ERR Failed to RETR that message, internal error")
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
@@ -453,12 +453,12 @@ 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) {
|
||||
reader, err := msg.RawReader()
|
||||
defer reader.Close()
|
||||
if err != nil {
|
||||
ses.logError("Failed to read message for RETR command")
|
||||
ses.send("-ERR Failed to RETR that message, internal error")
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
scanner := bufio.NewScanner(reader)
|
||||
inBody := false
|
||||
for scanner.Scan() {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package smtpd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/jhillyerd/go.enmime"
|
||||
"io"
|
||||
"net/mail"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrNotExist = errors.New("Message does not exist")
|
||||
|
||||
type DataStore interface {
|
||||
MailboxFor(emailAddress string) (Mailbox, error)
|
||||
AllMailboxes() ([]Mailbox, error)
|
||||
@@ -16,7 +19,7 @@ type Mailbox interface {
|
||||
GetMessages() ([]Message, error)
|
||||
GetMessage(id string) (Message, error)
|
||||
Purge() error
|
||||
NewMessage() Message
|
||||
NewMessage() (Message, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
|
||||
@@ -44,33 +44,31 @@ func countGenerator(c chan int) {
|
||||
// A DataStore is the root of the mail storage hiearchy. It provides access to
|
||||
// Mailbox objects
|
||||
type FileDataStore struct {
|
||||
path string
|
||||
mailPath string
|
||||
path string
|
||||
mailPath string
|
||||
messageCap int
|
||||
}
|
||||
|
||||
// NewFileDataStore creates a new DataStore object using the specified path
|
||||
func NewFileDataStore(path string) DataStore {
|
||||
func NewFileDataStore(cfg config.DataStoreConfig) DataStore {
|
||||
path := cfg.Path
|
||||
if path == "" {
|
||||
log.LogError("No value configured for datastore path")
|
||||
return nil
|
||||
}
|
||||
mailPath := filepath.Join(path, "mail")
|
||||
if _, err := os.Stat(mailPath); err != nil {
|
||||
// Mail datastore does not yet exist
|
||||
os.MkdirAll(mailPath, 0770)
|
||||
}
|
||||
return &FileDataStore{path: path, mailPath: mailPath}
|
||||
return &FileDataStore{path: path, mailPath: mailPath, messageCap: cfg.MailboxMsgCap}
|
||||
}
|
||||
|
||||
// DefaultFileDataStore creates a new DataStore object. It uses the inbucket.Config object to
|
||||
// construct it's path.
|
||||
func DefaultFileDataStore() DataStore {
|
||||
path, err := config.Config.String("datastore", "path")
|
||||
if err != nil {
|
||||
log.LogError("Error getting datastore path: %v", err)
|
||||
return nil
|
||||
}
|
||||
if path == "" {
|
||||
log.LogError("No value configured for datastore path")
|
||||
return nil
|
||||
}
|
||||
return NewFileDataStore(path)
|
||||
cfg := config.GetDataStoreConfig()
|
||||
return NewFileDataStore(cfg)
|
||||
}
|
||||
|
||||
// Retrieves the Mailbox object for a specified email address, if the mailbox
|
||||
@@ -178,7 +176,7 @@ func (mb *FileMailbox) GetMessage(id string) (Message, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Message %s not in index", id)
|
||||
return nil, ErrNotExist
|
||||
}
|
||||
|
||||
// Delete all messages in this mailbox
|
||||
@@ -292,11 +290,28 @@ type FileMessage struct {
|
||||
}
|
||||
|
||||
// NewMessage creates a new Message object and sets the Date and Id fields.
|
||||
func (mb *FileMailbox) NewMessage() Message {
|
||||
// It will also delete messages over messageCap if configured.
|
||||
func (mb *FileMailbox) NewMessage() (Message, error) {
|
||||
// Load index
|
||||
if !mb.indexLoaded {
|
||||
if err := mb.readIndex(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Delete old messages over messageCap
|
||||
if mb.store.messageCap > 0 {
|
||||
for len(mb.messages) >= mb.store.messageCap {
|
||||
log.LogInfo("Mailbox %q over configured message cap", mb.name)
|
||||
if err := mb.messages[0].Delete(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
date := time.Now()
|
||||
id := generateId(date)
|
||||
|
||||
return &FileMessage{mailbox: mb, Fid: id, Fdate: date, writable: true}
|
||||
return &FileMessage{mailbox: mb, Fid: id, Fdate: date, writable: true}, nil
|
||||
}
|
||||
|
||||
func (m *FileMessage) Id() string {
|
||||
@@ -330,10 +345,10 @@ func (m *FileMessage) rawPath() string {
|
||||
// ReadHeader opens the .raw portion of a Message and returns a standard Go mail.Message object
|
||||
func (m *FileMessage) ReadHeader() (msg *mail.Message, err error) {
|
||||
file, err := os.Open(m.rawPath())
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
reader := bufio.NewReader(file)
|
||||
msg, err = mail.ReadMessage(reader)
|
||||
return msg, err
|
||||
|
||||
@@ -3,6 +3,7 @@ package smtpd
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/jhillyerd/inbucket/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -15,7 +16,7 @@ import (
|
||||
|
||||
// Test directory structure created by filestore
|
||||
func TestFSDirStructure(t *testing.T) {
|
||||
ds, logbuf := setupDataStore()
|
||||
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||
defer teardownDataStore(ds)
|
||||
root := ds.path
|
||||
|
||||
@@ -99,7 +100,7 @@ func TestFSDirStructure(t *testing.T) {
|
||||
|
||||
// Test FileDataStore.AllMailboxes()
|
||||
func TestFSAllMailboxes(t *testing.T) {
|
||||
ds, logbuf := setupDataStore()
|
||||
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
for _, name := range []string{"abby", "bill", "christa", "donald", "evelyn"} {
|
||||
@@ -127,7 +128,7 @@ func TestFSAllMailboxes(t *testing.T) {
|
||||
// Test delivering several messages to the same mailbox, meanwhile querying its
|
||||
// contents with a new mailbox object each time
|
||||
func TestFSDeliverMany(t *testing.T) {
|
||||
ds, logbuf := setupDataStore()
|
||||
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
mbName := "fred"
|
||||
@@ -176,7 +177,7 @@ func TestFSDeliverMany(t *testing.T) {
|
||||
|
||||
// Test deleting messages
|
||||
func TestFSDelete(t *testing.T) {
|
||||
ds, logbuf := setupDataStore()
|
||||
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
mbName := "fred"
|
||||
@@ -250,7 +251,7 @@ func TestFSDelete(t *testing.T) {
|
||||
|
||||
// Test purging a mailbox
|
||||
func TestFSPurge(t *testing.T) {
|
||||
ds, logbuf := setupDataStore()
|
||||
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
mbName := "fred"
|
||||
@@ -298,7 +299,7 @@ func TestFSPurge(t *testing.T) {
|
||||
|
||||
// Test message size calculation
|
||||
func TestFSSize(t *testing.T) {
|
||||
ds, logbuf := setupDataStore()
|
||||
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
mbName := "fred"
|
||||
@@ -334,8 +335,130 @@ func TestFSSize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test missing files
|
||||
func TestFSMissing(t *testing.T) {
|
||||
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
mbName := "fred"
|
||||
subjects := []string{"a", "b", "c"}
|
||||
sentIds := make([]string, len(subjects))
|
||||
|
||||
for i, subj := range subjects {
|
||||
// Add a message
|
||||
id, _ := deliverMessage(ds, mbName, subj, time.Now())
|
||||
sentIds[i] = id
|
||||
}
|
||||
|
||||
mb, err := ds.MailboxFor(mbName)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to MailboxFor(%q): %v", mbName, err)
|
||||
}
|
||||
|
||||
// Delete a message file without removing it from index
|
||||
msg, err := mb.GetMessage(sentIds[1])
|
||||
assert.Nil(t, err)
|
||||
fmsg := msg.(*FileMessage)
|
||||
os.Remove(fmsg.rawPath())
|
||||
msg, err = mb.GetMessage(sentIds[1])
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Try to read parts of message
|
||||
_, err = msg.ReadHeader()
|
||||
assert.Error(t, err)
|
||||
_, err = msg.ReadBody()
|
||||
assert.Error(t, err)
|
||||
|
||||
if t.Failed() {
|
||||
// Wait for handler to finish logging
|
||||
time.Sleep(2 * time.Second)
|
||||
// Dump buffered log data if there was a failure
|
||||
io.Copy(os.Stderr, logbuf)
|
||||
}
|
||||
}
|
||||
|
||||
// Test delivering several messages to the same mailbox, see if message cap works
|
||||
func TestFSMessageCap(t *testing.T) {
|
||||
mbCap := 10
|
||||
ds, logbuf := setupDataStore(config.DataStoreConfig{MailboxMsgCap: mbCap})
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
mbName := "captain"
|
||||
for i := 0; i < 20; i++ {
|
||||
// Add a message
|
||||
subj := fmt.Sprintf("subject %v", i)
|
||||
deliverMessage(ds, mbName, subj, time.Now())
|
||||
t.Logf("Delivered %q", subj)
|
||||
|
||||
// Check number of messages
|
||||
mb, err := ds.MailboxFor(mbName)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to MailboxFor(%q): %v", mbName, err)
|
||||
}
|
||||
msgs, err := mb.GetMessages()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to GetMessages for %q: %v", mbName, err)
|
||||
}
|
||||
if len(msgs) > mbCap {
|
||||
t.Errorf("Mailbox should be capped at %v messages, but has %v", mbCap, len(msgs))
|
||||
}
|
||||
|
||||
// Check that the first message is correct
|
||||
first := i - mbCap + 1
|
||||
if first < 0 {
|
||||
first = 0
|
||||
}
|
||||
firstSubj := fmt.Sprintf("subject %v", first)
|
||||
if firstSubj != msgs[0].Subject() {
|
||||
t.Errorf("Expected first subject to be %q, got %q", firstSubj, msgs[0].Subject())
|
||||
}
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
// Wait for handler to finish logging
|
||||
time.Sleep(2 * time.Second)
|
||||
// Dump buffered log data if there was a failure
|
||||
io.Copy(os.Stderr, logbuf)
|
||||
}
|
||||
}
|
||||
|
||||
// Test delivering several messages to the same mailbox, see if no message cap works
|
||||
func TestFSNoMessageCap(t *testing.T) {
|
||||
mbCap := 0
|
||||
ds, logbuf := setupDataStore(config.DataStoreConfig{MailboxMsgCap: mbCap})
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
mbName := "captain"
|
||||
for i := 0; i < 20; i++ {
|
||||
// Add a message
|
||||
subj := fmt.Sprintf("subject %v", i)
|
||||
deliverMessage(ds, mbName, subj, time.Now())
|
||||
t.Logf("Delivered %q", subj)
|
||||
|
||||
// Check number of messages
|
||||
mb, err := ds.MailboxFor(mbName)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to MailboxFor(%q): %v", mbName, err)
|
||||
}
|
||||
msgs, err := mb.GetMessages()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to GetMessages for %q: %v", mbName, err)
|
||||
}
|
||||
if len(msgs) != i+1 {
|
||||
t.Errorf("Expected %v messages, got %v", i+1, len(msgs))
|
||||
}
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
// Wait for handler to finish logging
|
||||
time.Sleep(2 * time.Second)
|
||||
// Dump buffered log data if there was a failure
|
||||
io.Copy(os.Stderr, logbuf)
|
||||
}
|
||||
}
|
||||
|
||||
// setupDataStore creates a new FileDataStore in a temporary directory
|
||||
func setupDataStore() (*FileDataStore, *bytes.Buffer) {
|
||||
func setupDataStore(cfg config.DataStoreConfig) (*FileDataStore, *bytes.Buffer) {
|
||||
path, err := ioutil.TempDir("", "inbucket")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -345,12 +468,14 @@ func setupDataStore() (*FileDataStore, *bytes.Buffer) {
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
|
||||
return NewFileDataStore(path).(*FileDataStore), buf
|
||||
cfg.Path = path
|
||||
return NewFileDataStore(cfg).(*FileDataStore), buf
|
||||
}
|
||||
|
||||
// deliverMessage creates and delivers a message to the specific mailbox, returning
|
||||
// the size of the generated message.
|
||||
func deliverMessage(ds *FileDataStore, mbName string, subject string, date time.Time) (id string, size int) {
|
||||
func deliverMessage(ds *FileDataStore, mbName string, subject string,
|
||||
date time.Time) (id string, size int) {
|
||||
// Build fake SMTP message for delivery
|
||||
testMsg := make([]byte, 0, 300)
|
||||
testMsg = append(testMsg, []byte("To: somebody@host\r\n")...)
|
||||
@@ -365,12 +490,13 @@ func deliverMessage(ds *FileDataStore, mbName string, subject string, date time.
|
||||
}
|
||||
// Create message object
|
||||
id = generateId(date)
|
||||
msg := &FileMessage{
|
||||
mailbox: mb.(*FileMailbox),
|
||||
writable: true,
|
||||
Fdate: date,
|
||||
Fid: id,
|
||||
msg, err := mb.NewMessage()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmsg := msg.(*FileMessage)
|
||||
fmsg.Fdate = date
|
||||
fmsg.Fid = id
|
||||
msg.Append(testMsg)
|
||||
if err = msg.Close(); err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -24,6 +24,8 @@ const (
|
||||
QUIT // Close session
|
||||
)
|
||||
|
||||
const STAMP_FMT = "Mon, 02 Jan 2006 15:04:05 -0700 (MST)"
|
||||
|
||||
func (s State) String() string {
|
||||
switch s {
|
||||
case GREET:
|
||||
@@ -59,15 +61,16 @@ var commands = map[string]bool{
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
server *Server
|
||||
id int
|
||||
conn net.Conn
|
||||
remoteHost string
|
||||
sendError error
|
||||
state State
|
||||
reader *bufio.Reader
|
||||
from string
|
||||
recipients *list.List
|
||||
server *Server
|
||||
id int
|
||||
conn net.Conn
|
||||
remoteDomain string
|
||||
remoteHost string
|
||||
sendError error
|
||||
state State
|
||||
reader *bufio.Reader
|
||||
from string
|
||||
recipients *list.List
|
||||
}
|
||||
|
||||
func NewSession(server *Server, id int, conn net.Conn) *Session {
|
||||
@@ -196,9 +199,21 @@ func (s *Server) startSession(id int, conn net.Conn) {
|
||||
func (ss *Session) greetHandler(cmd string, arg string) {
|
||||
switch cmd {
|
||||
case "HELO":
|
||||
domain, err := parseHelloArgument(arg)
|
||||
if err != nil {
|
||||
ss.send("501 Domain/address argument required for HELO")
|
||||
return
|
||||
}
|
||||
ss.remoteDomain = domain
|
||||
ss.send("250 Great, let's get this show on the road")
|
||||
ss.enterState(READY)
|
||||
case "EHLO":
|
||||
domain, err := parseHelloArgument(arg)
|
||||
if err != nil {
|
||||
ss.send("501 Domain/address argument required for EHLO")
|
||||
return
|
||||
}
|
||||
ss.remoteDomain = domain
|
||||
ss.send("250-Great, let's get this show on the road")
|
||||
ss.send("250-8BITMIME")
|
||||
ss.send(fmt.Sprintf("250 SIZE %v", ss.server.maxMessageBytes))
|
||||
@@ -208,6 +223,17 @@ func (ss *Session) greetHandler(cmd string, arg string) {
|
||||
}
|
||||
}
|
||||
|
||||
func parseHelloArgument(arg string) (string, error) {
|
||||
domain := arg
|
||||
if idx := strings.IndexRune(arg, ' '); idx >= 0 {
|
||||
domain = arg[:idx]
|
||||
}
|
||||
if domain == "" {
|
||||
return "", fmt.Errorf("Invalid domain")
|
||||
}
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
// READY state -> waiting for MAIL
|
||||
func (ss *Session) readyHandler(cmd string, arg string) {
|
||||
if cmd == "MAIL" {
|
||||
@@ -305,11 +331,12 @@ func (ss *Session) mailHandler(cmd string, arg string) {
|
||||
|
||||
// DATA
|
||||
func (ss *Session) dataHandler() {
|
||||
msgSize := 0
|
||||
|
||||
// Timestamp for Received header
|
||||
stamp := time.Now().Format(STAMP_FMT)
|
||||
// Get a Mailbox and a new Message for each recipient
|
||||
mailboxes := make([]Mailbox, ss.recipients.Len())
|
||||
messages := make([]Message, ss.recipients.Len())
|
||||
msgSize := 0
|
||||
if ss.server.storeMessages {
|
||||
i := 0
|
||||
for e := ss.recipients.Front(); e != nil; e = e.Next() {
|
||||
@@ -325,13 +352,23 @@ func (ss *Session) dataHandler() {
|
||||
// Not our "no store" domain, so store the message
|
||||
mb, err := ss.server.dataStore.MailboxFor(local)
|
||||
if err != nil {
|
||||
ss.logError("Failed to open mailbox for %q", local)
|
||||
ss.logError("Failed to open mailbox for %q: %s", local, err)
|
||||
ss.send(fmt.Sprintf("451 Failed to open mailbox for %v", local))
|
||||
ss.reset()
|
||||
return
|
||||
}
|
||||
mailboxes[i] = mb
|
||||
messages[i] = mb.NewMessage()
|
||||
if messages[i], err = mb.NewMessage(); err != nil {
|
||||
ss.logError("Failed to create message for %q: %s", local, err)
|
||||
ss.send(fmt.Sprintf("451 Failed to create message for %v", local))
|
||||
ss.reset()
|
||||
return
|
||||
}
|
||||
|
||||
// Generate Received header
|
||||
recd := fmt.Sprintf("Received: from %s ([%s]) by %s\r\n for <%s>; %s\r\n",
|
||||
ss.remoteDomain, ss.remoteHost, ss.server.domain, recip, stamp)
|
||||
messages[i].Append([]byte(recd))
|
||||
} else {
|
||||
log.LogTrace("Not storing message for %q", recip)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ func TestGreetState(t *testing.T) {
|
||||
|
||||
// Test out some mangled HELOs
|
||||
script = []scriptStep{
|
||||
{"HELO", 501},
|
||||
{"EHLO", 501},
|
||||
{"HELLO", 500},
|
||||
{"HELL", 500},
|
||||
{"hello", 500},
|
||||
@@ -43,9 +45,6 @@ func TestGreetState(t *testing.T) {
|
||||
}
|
||||
|
||||
// Valid HELOs
|
||||
if err := playSession(t, server, []scriptStep{{"HELO", 250}}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := playSession(t, server, []scriptStep{{"HELO mydomain", 250}}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -55,6 +54,23 @@ func TestGreetState(t *testing.T) {
|
||||
if err := playSession(t, server, []scriptStep{{"HelO mydom.com", 250}}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := playSession(t, server, []scriptStep{{"helo 127.0.0.1", 250}}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Valid EHLOs
|
||||
if err := playSession(t, server, []scriptStep{{"EHLO mydomain", 250}}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := playSession(t, server, []scriptStep{{"EHLO mydom.com", 250}}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := playSession(t, server, []scriptStep{{"EhlO mydom.com", 250}}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := playSession(t, server, []scriptStep{{"ehlo 127.0.0.1", 250}}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
// Wait for handler to finish logging
|
||||
@@ -136,7 +152,7 @@ func TestMailState(t *testing.T) {
|
||||
mb1 := &MockMailbox{}
|
||||
msg1 := &MockMessage{}
|
||||
mds.On("MailboxFor").Return(mb1, nil)
|
||||
mb1.On("NewMessage").Return(msg1)
|
||||
mb1.On("NewMessage").Return(msg1, nil)
|
||||
msg1.On("Close").Return(nil)
|
||||
|
||||
server, logbuf := setupSmtpServer(mds)
|
||||
@@ -246,7 +262,7 @@ func TestDataState(t *testing.T) {
|
||||
mb1 := &MockMailbox{}
|
||||
msg1 := &MockMessage{}
|
||||
mds.On("MailboxFor").Return(mb1, nil)
|
||||
mb1.On("NewMessage").Return(msg1)
|
||||
mb1.On("NewMessage").Return(msg1, nil)
|
||||
msg1.On("Close").Return(nil)
|
||||
|
||||
server, logbuf := setupSmtpServer(mds)
|
||||
@@ -317,7 +333,7 @@ func playScriptAgainst(t *testing.T, c *textproto.Conn, script []scriptStep) err
|
||||
}
|
||||
|
||||
c.StartResponse(id)
|
||||
code, msg, err := c.ReadCodeLine(step.expect)
|
||||
code, msg, err := c.ReadResponse(step.expect)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Step %d, sent %q, expected %v, got %v: %q",
|
||||
i, step.send, step.expect, code, msg)
|
||||
|
||||
@@ -98,9 +98,9 @@ func (m *MockMailbox) Purge() error {
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockMailbox) NewMessage() Message {
|
||||
func (m *MockMailbox) NewMessage() (Message, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).(Message)
|
||||
return args.Get(0).(Message), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMailbox) String() string {
|
||||
|
||||
8
swaks-tests/nonmime-html.raw
Normal file
8
swaks-tests/nonmime-html.raw
Normal file
@@ -0,0 +1,8 @@
|
||||
Date: %DATE%
|
||||
To: %TO_ADDRESS%
|
||||
From: %FROM_ADDRESS%
|
||||
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.
|
||||
@@ -7,7 +7,10 @@ export SWAKS_OPT_to="swaks@inbucket.local"
|
||||
swaks $* --h-Subject: "Swaks Plain Text" --body text.txt
|
||||
|
||||
# HTML test
|
||||
swaks $* --h-Subject: "Swaks HTML" --data html.raw
|
||||
swaks $* --h-Subject: "Swaks HTML" --data mime-html.raw
|
||||
|
||||
# Top level HTML test
|
||||
swaks $* --h-Subject: "Swaks Top Level HTML" --data nonmime-html.raw
|
||||
|
||||
# Attachment test
|
||||
swaks $* --h-Subject: "Swaks Attachment" --attach-type image/png --attach favicon.png --body text.txt
|
||||
|
||||
@@ -320,7 +320,7 @@ table.metrics {
|
||||
width: 15em;
|
||||
}
|
||||
|
||||
.metrics td {
|
||||
.metrics td.number {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ Released for free under a Creative Commons Attribution 2.5 License
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer">
|
||||
<p>Inbucket is an open source project hosted at
|
||||
<a href="http://jhillyerd.github.io/inbucket/">github</a>.
|
||||
<p><a href="http://inbucket.org/">Inbucket</a> is an open source project hosted at
|
||||
<a href="http://github.com/jhillyerd/inbucket">github</a>.
|
||||
Design by <a href="http://www.freecsstemplates.org/">FCT</a>.</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
{{define "script"}}
|
||||
<script>
|
||||
var selected = "{{.selected}}"
|
||||
function messageLoaded(responseText, textStatus, XMLHttpRequest) {
|
||||
if (textStatus == "error") {
|
||||
alert("Failed to load message, server said:\n" + responseText)
|
||||
} else {
|
||||
window.scrollTo(0,0)
|
||||
}
|
||||
}
|
||||
|
||||
function listLoaded() {
|
||||
$('.listEntry').hover(
|
||||
function() {
|
||||
@@ -16,7 +24,7 @@
|
||||
function() {
|
||||
$('.listEntry').removeClass("listEntrySelected")
|
||||
$(this).addClass("listEntrySelected")
|
||||
$('#emailContent').load('/mailbox/{{.name}}/' + this.id)
|
||||
$('#emailContent').load('/mailbox/{{.name}}/' + this.id, messageLoaded)
|
||||
}
|
||||
)
|
||||
$("#messageList").slideDown()
|
||||
|
||||
@@ -154,6 +154,10 @@
|
||||
<div class="box">
|
||||
<h3>Configuration</h3>
|
||||
<table class="metrics">
|
||||
<tr>
|
||||
<th>Version:</th>
|
||||
<td><span>{{.version}}, built on {{.buildDate}}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>SMTP Listener:</th>
|
||||
<td><span>{{.smtpListener}}</span></td>
|
||||
@@ -174,29 +178,29 @@
|
||||
<table class="metrics">
|
||||
<tr>
|
||||
<th>Uptime:</th>
|
||||
<td><span id="m-uptime">.</span></td>
|
||||
<td class="number"><span id="m-uptime">.</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>System Memory:</th>
|
||||
<td><span id="m-memstatsSys">.</span></td>
|
||||
<td class="number"><span id="m-memstatsSys">.</span></td>
|
||||
<td class="sparkline"><span id="s-memstatsSys">.</span></td>
|
||||
<td>(10min)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Heap Size:</th>
|
||||
<td><span id="m-memstatsHeapSys">.</span></td>
|
||||
<td class="number"><span id="m-memstatsHeapSys">.</span></td>
|
||||
<td class="sparkline"><span id="s-memstatsHeapSys">.</span></td>
|
||||
<td>(10min)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Heap In-Use:</th>
|
||||
<td><span id="m-memstatsHeapAlloc">.</span></td>
|
||||
<td class="number"><span id="m-memstatsHeapAlloc">.</span></td>
|
||||
<td class="sparkline"><span id="s-memstatsHeapAlloc">.</span></td>
|
||||
<td>(10min)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Heap # Objects:</th>
|
||||
<td><span id="m-memstatsHeapObjects">.</span></td>
|
||||
<td class="number"><span id="m-memstatsHeapObjects">.</span></td>
|
||||
<td class="sparkline"><span id="s-memstatsHeapObjects">.</span></td>
|
||||
<td>(10min)</td>
|
||||
</tr>
|
||||
@@ -208,31 +212,31 @@
|
||||
<table class="metrics">
|
||||
<tr>
|
||||
<th>Current Connections:</th>
|
||||
<td><span id="m-smtpConnectsCurrent">.</span></td>
|
||||
<td class="number"><span id="m-smtpConnectsCurrent">.</span></td>
|
||||
<td class="sparkline"><span id="s-smtpConnectsCurrent">.</span></td>
|
||||
<td>(10min)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total Connections:</th>
|
||||
<td><span id="m-smtpConnectsTotal">.</span></td>
|
||||
<td class="number"><span id="m-smtpConnectsTotal">.</span></td>
|
||||
<td class="sparkline"><span id="s-smtpConnectsTotal">.</span></td>
|
||||
<td>(60min)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Messages Received:</th>
|
||||
<td><span id="m-smtpReceivedTotal">.</span></td>
|
||||
<td class="number"><span id="m-smtpReceivedTotal">.</span></td>
|
||||
<td class="sparkline"><span id="s-smtpReceivedTotal">.</span></td>
|
||||
<td>(60min)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Errors Logged:</th>
|
||||
<td><span id="m-smtpErrorsTotal">.</span></td>
|
||||
<td class="number"><span id="m-smtpErrorsTotal">.</span></td>
|
||||
<td class="sparkline"><span id="s-smtpErrorsTotal"></span></td>
|
||||
<td>(60min)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Warnings Logged:</th>
|
||||
<td><span id="m-smtpWarnsTotal">.</span></td>
|
||||
<td class="number"><span id="m-smtpWarnsTotal">.</span></td>
|
||||
<td class="sparkline"><span id="s-smtpWarnsTotal"></span></td>
|
||||
<td>(60min)</td>
|
||||
</tr>
|
||||
@@ -264,13 +268,13 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Retention Deletes:</th>
|
||||
<td><span id="m-retentionDeletesTotal">.</span></td>
|
||||
<td class="number"><span id="m-retentionDeletesTotal">.</span></td>
|
||||
<td class="sparkline"><span id="s-retentionDeletesTotal"></span></td>
|
||||
<td>(60min)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Currently Retained:</th>
|
||||
<td><span id="m-retainedCurrent">.</span></td>
|
||||
<td class="number"><span id="m-retainedCurrent">.</span></td>
|
||||
<td class="sparkline"><span id="s-retainedCurrent"></span></td>
|
||||
<td>(60min)</td>
|
||||
</tr>
|
||||
|
||||
@@ -120,6 +120,10 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
|
||||
return fmt.Errorf("MailboxFor('%v'): %v", name, err)
|
||||
}
|
||||
msg, err := mb.GetMessage(id)
|
||||
if err == smtpd.ErrNotExist {
|
||||
http.NotFound(w, req)
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetMessage() failed: %v", err)
|
||||
}
|
||||
@@ -206,6 +210,7 @@ func MailboxHtml(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
|
||||
return err
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
||||
return RenderPartial("mailbox/_html.html", w, map[string]interface{}{
|
||||
"ctx": ctx,
|
||||
"name": name,
|
||||
|
||||
519
web/rest_test.go
Normal file
519
web/rest_test.go
Normal file
@@ -0,0 +1,519 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jhillyerd/go.enmime"
|
||||
"github.com/jhillyerd/inbucket/config"
|
||||
"github.com/jhillyerd/inbucket/smtpd"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/mail"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type OutputJsonHeader struct {
|
||||
Mailbox, Id, From, Subject, Date string
|
||||
Size int
|
||||
}
|
||||
|
||||
type OutputJsonMessage struct {
|
||||
Mailbox, Id, From, Subject, Date string
|
||||
Size int
|
||||
Header map[string][]string
|
||||
Body struct {
|
||||
Text, Html string
|
||||
}
|
||||
}
|
||||
|
||||
type InputMessageData struct {
|
||||
Mailbox, Id, From, Subject string
|
||||
Date time.Time
|
||||
Size int
|
||||
Header mail.Header
|
||||
Html, Text string
|
||||
}
|
||||
|
||||
func (d *InputMessageData) MockMessage() *MockMessage {
|
||||
msg := &MockMessage{}
|
||||
msg.On("Id").Return(d.Id)
|
||||
msg.On("From").Return(d.From)
|
||||
msg.On("Subject").Return(d.Subject)
|
||||
msg.On("Date").Return(d.Date)
|
||||
msg.On("Size").Return(d.Size)
|
||||
gomsg := &mail.Message{
|
||||
Header: d.Header,
|
||||
}
|
||||
msg.On("ReadHeader").Return(gomsg, nil)
|
||||
body := &enmime.MIMEBody{
|
||||
Text: d.Text,
|
||||
Html: d.Html,
|
||||
}
|
||||
msg.On("ReadBody").Return(body, nil)
|
||||
return msg
|
||||
}
|
||||
|
||||
func (d *InputMessageData) CompareToJsonHeader(j *OutputJsonHeader) (errors []string) {
|
||||
if d.Mailbox != j.Mailbox {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.Mailbox=%q, got %q", d.Mailbox,
|
||||
j.Mailbox))
|
||||
}
|
||||
if d.Id != j.Id {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.Id=%q, got %q", d.Id,
|
||||
j.Id))
|
||||
}
|
||||
if d.From != j.From {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.From=%q, got %q", d.From,
|
||||
j.From))
|
||||
}
|
||||
if d.Subject != j.Subject {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.Subject=%q, got %q", d.Subject,
|
||||
j.Subject))
|
||||
}
|
||||
exDate := d.Date.Format("2006-01-02T15:04:05.999999999-07:00")
|
||||
if exDate != j.Date {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.Date=%q, got %q", exDate,
|
||||
j.Date))
|
||||
}
|
||||
if d.Size != j.Size {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.Size=%v, got %v", d.Size,
|
||||
j.Size))
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func (d *InputMessageData) CompareToJsonMessage(j *OutputJsonMessage) (errors []string) {
|
||||
if d.Mailbox != j.Mailbox {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.Mailbox=%q, got %q", d.Mailbox,
|
||||
j.Mailbox))
|
||||
}
|
||||
if d.Id != j.Id {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.Id=%q, got %q", d.Id,
|
||||
j.Id))
|
||||
}
|
||||
if d.From != j.From {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.From=%q, got %q", d.From,
|
||||
j.From))
|
||||
}
|
||||
if d.Subject != j.Subject {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.Subject=%q, got %q", d.Subject,
|
||||
j.Subject))
|
||||
}
|
||||
exDate := d.Date.Format("2006-01-02T15:04:05.999999999-07:00")
|
||||
if exDate != j.Date {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.Date=%q, got %q", exDate,
|
||||
j.Date))
|
||||
}
|
||||
if d.Size != j.Size {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.Size=%v, got %v", d.Size,
|
||||
j.Size))
|
||||
}
|
||||
if d.Text != j.Body.Text {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.Text=%q, got %q", d.Text,
|
||||
j.Body.Text))
|
||||
}
|
||||
if d.Html != j.Body.Html {
|
||||
errors = append(errors, fmt.Sprintf("Expected JSON.Html=%q, got %q", d.Html,
|
||||
j.Body.Html))
|
||||
}
|
||||
for k, vals := range d.Header {
|
||||
jvals, ok := j.Header[k]
|
||||
if ok {
|
||||
for _, v := range vals {
|
||||
hasValue := false
|
||||
for _, jv := range jvals {
|
||||
if v == jv {
|
||||
hasValue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasValue {
|
||||
errors = append(errors, fmt.Sprintf("JSON.Header[%q] missing value %q", k, v))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors = append(errors, fmt.Sprintf("JSON.Header missing key %q", k))
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func TestRestMailboxList(t *testing.T) {
|
||||
// Setup
|
||||
ds := &MockDataStore{}
|
||||
logbuf := setupWebServer(ds)
|
||||
|
||||
// Test invalid mailbox name
|
||||
w, err := testRestGet("http://localhost/mailbox/foo@bar")
|
||||
expectCode := 500
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Code != expectCode {
|
||||
t.Errorf("Expected code %v, got %v", expectCode, w.Code)
|
||||
}
|
||||
|
||||
// Test empty mailbox
|
||||
emptybox := &MockMailbox{}
|
||||
ds.On("MailboxFor", "empty").Return(emptybox, nil)
|
||||
emptybox.On("GetMessages").Return([]smtpd.Message{}, nil)
|
||||
|
||||
w, err = testRestGet("http://localhost/mailbox/empty")
|
||||
expectCode = 200
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Code != expectCode {
|
||||
t.Errorf("Expected code %v, got %v", expectCode, w.Code)
|
||||
}
|
||||
|
||||
// Test MailboxFor error
|
||||
ds.On("MailboxFor", "error").Return(&MockMailbox{}, fmt.Errorf("Internal error"))
|
||||
w, err = testRestGet("http://localhost/mailbox/error")
|
||||
expectCode = 500
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Code != expectCode {
|
||||
t.Errorf("Expected code %v, got %v", expectCode, w.Code)
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
// Wait for handler to finish logging
|
||||
time.Sleep(2 * time.Second)
|
||||
// Dump buffered log data if there was a failure
|
||||
io.Copy(os.Stderr, logbuf)
|
||||
}
|
||||
|
||||
// Test MailboxFor error
|
||||
error2box := &MockMailbox{}
|
||||
ds.On("MailboxFor", "error2").Return(error2box, nil)
|
||||
error2box.On("GetMessages").Return([]smtpd.Message{}, fmt.Errorf("Internal error 2"))
|
||||
|
||||
w, err = testRestGet("http://localhost/mailbox/error2")
|
||||
expectCode = 500
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Code != expectCode {
|
||||
t.Errorf("Expected code %v, got %v", expectCode, w.Code)
|
||||
}
|
||||
|
||||
// Test JSON message headers
|
||||
data1 := &InputMessageData{
|
||||
Mailbox: "good",
|
||||
Id: "0001",
|
||||
From: "from1",
|
||||
Subject: "subject 1",
|
||||
Date: time.Date(2012, 2, 1, 10, 11, 12, 253, time.FixedZone("PST", -800)),
|
||||
}
|
||||
data2 := &InputMessageData{
|
||||
Mailbox: "good",
|
||||
Id: "0002",
|
||||
From: "from2",
|
||||
Subject: "subject 2",
|
||||
Date: time.Date(2012, 7, 1, 10, 11, 12, 253, time.FixedZone("PDT", -700)),
|
||||
}
|
||||
goodbox := &MockMailbox{}
|
||||
ds.On("MailboxFor", "good").Return(goodbox, nil)
|
||||
msg1 := data1.MockMessage()
|
||||
msg2 := data2.MockMessage()
|
||||
goodbox.On("GetMessages").Return([]smtpd.Message{msg1, msg2}, nil)
|
||||
|
||||
// Check return code
|
||||
w, err = testRestGet("http://localhost/mailbox/good")
|
||||
expectCode = 200
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Code != expectCode {
|
||||
t.Fatalf("Expected code %v, got %v", expectCode, w.Code)
|
||||
}
|
||||
|
||||
// Check JSON
|
||||
dec := json.NewDecoder(w.Body)
|
||||
var result []OutputJsonHeader
|
||||
if err := dec.Decode(&result); err != nil {
|
||||
t.Errorf("Failed to decode JSON: %v", err)
|
||||
}
|
||||
if len(result) != 2 {
|
||||
t.Errorf("Expected 2 results, got %v", len(result))
|
||||
}
|
||||
if errors := data1.CompareToJsonHeader(&result[0]); len(errors) > 0 {
|
||||
for _, e := range errors {
|
||||
t.Error(e)
|
||||
}
|
||||
}
|
||||
if errors := data2.CompareToJsonHeader(&result[1]); len(errors) > 0 {
|
||||
for _, e := range errors {
|
||||
t.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
// Wait for handler to finish logging
|
||||
time.Sleep(2 * time.Second)
|
||||
// Dump buffered log data if there was a failure
|
||||
io.Copy(os.Stderr, logbuf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestMessage(t *testing.T) {
|
||||
// Setup
|
||||
ds := &MockDataStore{}
|
||||
logbuf := setupWebServer(ds)
|
||||
|
||||
// Test invalid mailbox name
|
||||
w, err := testRestGet("http://localhost/mailbox/foo@bar/0001")
|
||||
expectCode := 500
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Code != expectCode {
|
||||
t.Errorf("Expected code %v, got %v", expectCode, w.Code)
|
||||
}
|
||||
|
||||
// Test requesting a message that does not exist
|
||||
emptybox := &MockMailbox{}
|
||||
ds.On("MailboxFor", "empty").Return(emptybox, nil)
|
||||
emptybox.On("GetMessage", "0001").Return(&MockMessage{}, smtpd.ErrNotExist)
|
||||
|
||||
w, err = testRestGet("http://localhost/mailbox/empty/0001")
|
||||
expectCode = 404
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Code != expectCode {
|
||||
t.Errorf("Expected code %v, got %v", expectCode, w.Code)
|
||||
}
|
||||
|
||||
// Test MailboxFor error
|
||||
ds.On("MailboxFor", "error").Return(&MockMailbox{}, fmt.Errorf("Internal error"))
|
||||
w, err = testRestGet("http://localhost/mailbox/error/0001")
|
||||
expectCode = 500
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Code != expectCode {
|
||||
t.Errorf("Expected code %v, got %v", expectCode, w.Code)
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
// Wait for handler to finish logging
|
||||
time.Sleep(2 * time.Second)
|
||||
// Dump buffered log data if there was a failure
|
||||
io.Copy(os.Stderr, logbuf)
|
||||
}
|
||||
|
||||
// Test GetMessage error
|
||||
error2box := &MockMailbox{}
|
||||
ds.On("MailboxFor", "error2").Return(error2box, nil)
|
||||
error2box.On("GetMessage", "0001").Return(&MockMessage{}, fmt.Errorf("Internal error 2"))
|
||||
|
||||
w, err = testRestGet("http://localhost/mailbox/error2/0001")
|
||||
expectCode = 500
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Code != expectCode {
|
||||
t.Errorf("Expected code %v, got %v", expectCode, w.Code)
|
||||
}
|
||||
|
||||
// Test JSON message headers
|
||||
data1 := &InputMessageData{
|
||||
Mailbox: "good",
|
||||
Id: "0001",
|
||||
From: "from1",
|
||||
Subject: "subject 1",
|
||||
Date: time.Date(2012, 2, 1, 10, 11, 12, 253, time.FixedZone("PST", -800)),
|
||||
Header: mail.Header{
|
||||
"To": []string{"fred@fish.com", "keyword@nsa.gov"},
|
||||
},
|
||||
Text: "This is some text",
|
||||
Html: "This is some HTML",
|
||||
}
|
||||
goodbox := &MockMailbox{}
|
||||
ds.On("MailboxFor", "good").Return(goodbox, nil)
|
||||
msg1 := data1.MockMessage()
|
||||
goodbox.On("GetMessage", "0001").Return(msg1, nil)
|
||||
|
||||
// Check return code
|
||||
w, err = testRestGet("http://localhost/mailbox/good/0001")
|
||||
expectCode = 200
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Code != expectCode {
|
||||
t.Fatalf("Expected code %v, got %v", expectCode, w.Code)
|
||||
}
|
||||
|
||||
// Check JSON
|
||||
dec := json.NewDecoder(w.Body)
|
||||
var result OutputJsonMessage
|
||||
if err := dec.Decode(&result); err != nil {
|
||||
t.Errorf("Failed to decode JSON: %v", err)
|
||||
}
|
||||
if errors := data1.CompareToJsonMessage(&result); len(errors) > 0 {
|
||||
for _, e := range errors {
|
||||
t.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
// Wait for handler to finish logging
|
||||
time.Sleep(2 * time.Second)
|
||||
// Dump buffered log data if there was a failure
|
||||
io.Copy(os.Stderr, logbuf)
|
||||
}
|
||||
}
|
||||
|
||||
func testRestGet(url string) (*httptest.ResponseRecorder, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
req.Header.Add("Accept", "application/json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
Router.ServeHTTP(w, req)
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func setupWebServer(ds smtpd.DataStore) *bytes.Buffer {
|
||||
// Capture log output
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
|
||||
// Have to reset default mux to prevent duplicate routes
|
||||
http.DefaultServeMux = http.NewServeMux()
|
||||
cfg := config.WebConfig{
|
||||
TemplateDir: "../themes/integral/templates",
|
||||
PublicDir: "../themes/integral/public",
|
||||
}
|
||||
Initialize(cfg, ds)
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// Mock DataStore object
|
||||
type MockDataStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockDataStore) MailboxFor(name string) (smtpd.Mailbox, error) {
|
||||
args := m.Called(name)
|
||||
return args.Get(0).(smtpd.Mailbox), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockDataStore) AllMailboxes() ([]smtpd.Mailbox, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).([]smtpd.Mailbox), args.Error(1)
|
||||
}
|
||||
|
||||
// Mock Mailbox object
|
||||
type MockMailbox struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockMailbox) GetMessages() ([]smtpd.Message, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).([]smtpd.Message), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMailbox) GetMessage(id string) (smtpd.Message, error) {
|
||||
args := m.Called(id)
|
||||
return args.Get(0).(smtpd.Message), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMailbox) Purge() error {
|
||||
args := m.Called()
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockMailbox) NewMessage() (smtpd.Message, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).(smtpd.Message), args.Error(1)
|
||||
}
|
||||
|
||||
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) 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.MIMEBody, err error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).(*enmime.MIMEBody), 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)
|
||||
}
|
||||
@@ -13,9 +13,9 @@ func RootIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err erro
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to load greeting: %v", err)
|
||||
}
|
||||
|
||||
|
||||
return RenderTemplate("root/index.html", w, map[string]interface{}{
|
||||
"ctx": ctx,
|
||||
"ctx": ctx,
|
||||
"greeting": template.HTML(string(greeting)),
|
||||
})
|
||||
}
|
||||
@@ -30,6 +30,8 @@ func RootStatus(w http.ResponseWriter, req *http.Request, ctx *Context) (err err
|
||||
config.GetWebConfig().Ip4port)
|
||||
return RenderTemplate("root/status.html", w, map[string]interface{}{
|
||||
"ctx": ctx,
|
||||
"version": config.VERSION,
|
||||
"buildDate": config.BUILD_DATE,
|
||||
"retentionMinutes": retentionMinutes,
|
||||
"smtpListener": smtpListener,
|
||||
"pop3Listener": pop3Listener,
|
||||
|
||||
@@ -18,12 +18,25 @@ import (
|
||||
|
||||
type handler func(http.ResponseWriter, *http.Request, *Context) error
|
||||
|
||||
var webConfig config.WebConfig
|
||||
var DataStore smtpd.DataStore
|
||||
var Router *mux.Router
|
||||
var listener net.Listener
|
||||
var sessionStore sessions.Store
|
||||
var shutdown bool
|
||||
|
||||
// Initialize sets up things for unit tests or the Start() method
|
||||
func Initialize(cfg config.WebConfig, ds smtpd.DataStore) {
|
||||
webConfig = cfg
|
||||
setupRoutes(cfg)
|
||||
|
||||
// NewContext() will use this DataStore for the web handlers
|
||||
DataStore = ds
|
||||
|
||||
// TODO Make configurable
|
||||
sessionStore = sessions.NewCookieStore([]byte("something-very-secret"))
|
||||
}
|
||||
|
||||
func setupRoutes(cfg config.WebConfig) {
|
||||
log.LogInfo("Theme templates mapped to '%v'", cfg.TemplateDir)
|
||||
log.LogInfo("Theme static content mapped to '%v'", cfg.PublicDir)
|
||||
@@ -54,16 +67,7 @@ func setupRoutes(cfg config.WebConfig) {
|
||||
|
||||
// Start() the web server
|
||||
func Start() {
|
||||
cfg := config.GetWebConfig()
|
||||
setupRoutes(cfg)
|
||||
|
||||
// NewContext() will use this DataStore for the web handlers
|
||||
DataStore = smtpd.DefaultFileDataStore()
|
||||
|
||||
// TODO Make configurable
|
||||
sessionStore = sessions.NewCookieStore([]byte("something-very-secret"))
|
||||
|
||||
addr := fmt.Sprintf("%v:%v", cfg.Ip4address, cfg.Ip4port)
|
||||
addr := fmt.Sprintf("%v:%v", webConfig.Ip4address, webConfig.Ip4port)
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: nil,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/jhillyerd/inbucket/config"
|
||||
"github.com/jhillyerd/inbucket/log"
|
||||
"html/template"
|
||||
"net/http"
|
||||
@@ -49,9 +48,8 @@ func ParseTemplate(name string, partial bool) (*template.Template, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
cfg := config.GetWebConfig()
|
||||
tempPath := strings.Replace(name, "/", string(filepath.Separator), -1)
|
||||
tempFile := filepath.Join(cfg.TemplateDir, tempPath)
|
||||
tempFile := filepath.Join(webConfig.TemplateDir, tempPath)
|
||||
log.LogTrace("Parsing template %v", tempFile)
|
||||
|
||||
var err error
|
||||
@@ -63,14 +61,14 @@ func ParseTemplate(name string, partial bool) (*template.Template, error) {
|
||||
t, err = t.ParseFiles(tempFile)
|
||||
} else {
|
||||
t = template.New("_base.html").Funcs(TemplateFuncs)
|
||||
t, err = t.ParseFiles(filepath.Join(cfg.TemplateDir, "_base.html"), tempFile)
|
||||
t, err = t.ParseFiles(filepath.Join(webConfig.TemplateDir, "_base.html"), tempFile)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Allows us to disable caching for theme development
|
||||
if cfg.TemplateCache {
|
||||
if webConfig.TemplateCache {
|
||||
if partial {
|
||||
log.LogTrace("Caching partial %v", name)
|
||||
cachedTemplates[name] = t
|
||||
|
||||
Reference in New Issue
Block a user