mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-18 10:07:02 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c34549e783 | ||
|
|
13868d85d4 | ||
|
|
8f10e18fef | ||
|
|
b105bbf87f | ||
|
|
ad85a1db93 | ||
|
|
d418f4ba29 | ||
|
|
d98e6a2b58 | ||
|
|
8b7fbfda6a | ||
|
|
46fa714cc7 | ||
|
|
414ed44882 | ||
|
|
2e1c937d23 | ||
|
|
df11575b3a |
@@ -8,6 +8,6 @@
|
|||||||
"Include": "README*,LICENSE*,inbucket.bat,etc,themes"
|
"Include": "README*,LICENSE*,inbucket.bat,etc,themes"
|
||||||
},
|
},
|
||||||
"PackageVersion": "1.0",
|
"PackageVersion": "1.0",
|
||||||
"PrereleaseInfo": "rc2",
|
"PrereleaseInfo": "",
|
||||||
"FormatVersion": "0.8"
|
"FormatVersion": "0.8"
|
||||||
}
|
}
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -2,34 +2,24 @@ Inbucket [.
|
||||||
|
|
||||||
It allows web developers, software engineers and system administrators to
|
Read more at the [Inbucket website][Inbucket]
|
||||||
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/).
|
|
||||||
|
|
||||||
Development Status
|
Development Status
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
Inbucket is currently release-candidate quality: it is being used for real work.
|
Inbucket is currently production 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.
|
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:
|
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
|
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/).
|
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
|
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
|
Inbucket is open source software released under the MIT License. The latest
|
||||||
version can be found at https://github.com/jhillyerd/inbucket
|
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
|
Path string
|
||||||
RetentionMinutes int
|
RetentionMinutes int
|
||||||
RetentionSleep int
|
RetentionSleep int
|
||||||
|
MailboxMsgCap int
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// Build info, set by main
|
||||||
|
VERSION = ""
|
||||||
|
BUILD_DATE = ""
|
||||||
|
|
||||||
// Global goconfig object
|
// Global goconfig object
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
|
|
||||||
@@ -121,6 +126,7 @@ func LoadConfig(filename string) error {
|
|||||||
requireOption(messages, "datastore", "path")
|
requireOption(messages, "datastore", "path")
|
||||||
requireOption(messages, "datastore", "retention.minutes")
|
requireOption(messages, "datastore", "retention.minutes")
|
||||||
requireOption(messages, "datastore", "retention.sleep.millis")
|
requireOption(messages, "datastore", "retention.sleep.millis")
|
||||||
|
requireOption(messages, "datastore", "mailbox.message.cap")
|
||||||
|
|
||||||
// Return error if validations failed
|
// Return error if validations failed
|
||||||
if messages.Len() > 0 {
|
if messages.Len() > 0 {
|
||||||
@@ -361,6 +367,11 @@ func parseDataStoreConfig() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,3 +100,8 @@ retention.minutes=0
|
|||||||
# This should help reduce disk I/O when there are a large number of messages
|
# This should help reduce disk I/O when there are a large number of messages
|
||||||
# to purge.
|
# to purge.
|
||||||
retention.sleep.millis=100
|
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
|
# This should help reduce disk I/O when there are a large number of messages
|
||||||
# to purge.
|
# to purge.
|
||||||
retention.sleep.millis=100
|
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
|
# This should help reduce disk I/O when there are a large number of messages
|
||||||
# to purge.
|
# to purge.
|
||||||
retention.sleep.millis=100
|
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
|
# This should help reduce disk I/O when there are a large number of messages
|
||||||
# to purge.
|
# to purge.
|
||||||
retention.sleep.millis=100
|
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"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command line flags
|
var (
|
||||||
var help = flag.Bool("help", false, "Displays this help")
|
// Build info, populated during linking by goxc
|
||||||
var pidfile = flag.String("pidfile", "none", "Write our PID into the specified file")
|
VERSION = "1.0"
|
||||||
var logfile = flag.String("logfile", "stderr", "Write out log into the specified file")
|
BUILD_DATE = "undefined"
|
||||||
|
|
||||||
// startTime is used to calculate uptime of Inbucket
|
// Command line flags
|
||||||
var startTime = time.Now()
|
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
|
// startTime is used to calculate uptime of Inbucket
|
||||||
var logf *os.File
|
startTime = time.Now()
|
||||||
|
|
||||||
var smtpServer *smtpd.Server
|
// The file we send log output to, will be nil for stderr or stdout
|
||||||
var pop3Server *pop3d.Server
|
logf *os.File
|
||||||
|
|
||||||
|
// Server instances
|
||||||
|
smtpServer *smtpd.Server
|
||||||
|
pop3Server *pop3d.Server
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
config.VERSION = VERSION
|
||||||
|
config.BUILD_DATE = BUILD_DATE
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if *help {
|
if *help {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
@@ -82,6 +92,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.LogInfo("Inbucket %v (%v) starting...", config.VERSION, config.BUILD_DATE)
|
||||||
|
|
||||||
// Write pidfile if requested
|
// Write pidfile if requested
|
||||||
// TODO: Probably supposed to remove pidfile during shutdown
|
// TODO: Probably supposed to remove pidfile during shutdown
|
||||||
if *pidfile != "none" {
|
if *pidfile != "none" {
|
||||||
@@ -98,6 +110,7 @@ func main() {
|
|||||||
ds := smtpd.DefaultFileDataStore()
|
ds := smtpd.DefaultFileDataStore()
|
||||||
|
|
||||||
// Start HTTP server
|
// Start HTTP server
|
||||||
|
web.Initialize(config.GetWebConfig(), ds)
|
||||||
go web.Start()
|
go web.Start()
|
||||||
|
|
||||||
// Start POP3 server
|
// Start POP3 server
|
||||||
|
|||||||
@@ -425,12 +425,12 @@ func (ses *Session) transactionHandler(cmd string, args []string) {
|
|||||||
// Send the contents of the message to the client
|
// Send the contents of the message to the client
|
||||||
func (ses *Session) sendMessage(msg smtpd.Message) {
|
func (ses *Session) sendMessage(msg smtpd.Message) {
|
||||||
reader, err := msg.RawReader()
|
reader, err := msg.RawReader()
|
||||||
defer reader.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ses.logError("Failed to read message for RETR command")
|
ses.logError("Failed to read message for RETR command")
|
||||||
ses.send("-ERR Failed to RETR that message, internal error")
|
ses.send("-ERR Failed to RETR that message, internal error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer reader.Close()
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
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
|
// 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 smtpd.Message, lineCount int) {
|
||||||
reader, err := msg.RawReader()
|
reader, err := msg.RawReader()
|
||||||
defer reader.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ses.logError("Failed to read message for RETR command")
|
ses.logError("Failed to read message for RETR command")
|
||||||
ses.send("-ERR Failed to RETR that message, internal error")
|
ses.send("-ERR Failed to RETR that message, internal error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer reader.Close()
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
inBody := false
|
inBody := false
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package smtpd
|
package smtpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/jhillyerd/go.enmime"
|
"github.com/jhillyerd/go.enmime"
|
||||||
"io"
|
"io"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrNotExist = errors.New("Message does not exist")
|
||||||
|
|
||||||
type DataStore interface {
|
type DataStore interface {
|
||||||
MailboxFor(emailAddress string) (Mailbox, error)
|
MailboxFor(emailAddress string) (Mailbox, error)
|
||||||
AllMailboxes() ([]Mailbox, error)
|
AllMailboxes() ([]Mailbox, error)
|
||||||
@@ -16,7 +19,7 @@ type Mailbox interface {
|
|||||||
GetMessages() ([]Message, error)
|
GetMessages() ([]Message, error)
|
||||||
GetMessage(id string) (Message, error)
|
GetMessage(id string) (Message, error)
|
||||||
Purge() error
|
Purge() error
|
||||||
NewMessage() Message
|
NewMessage() (Message, error)
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,31 +46,29 @@ func countGenerator(c chan int) {
|
|||||||
type FileDataStore struct {
|
type FileDataStore struct {
|
||||||
path string
|
path string
|
||||||
mailPath string
|
mailPath string
|
||||||
|
messageCap int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFileDataStore creates a new DataStore object using the specified path
|
// 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")
|
mailPath := filepath.Join(path, "mail")
|
||||||
if _, err := os.Stat(mailPath); err != nil {
|
if _, err := os.Stat(mailPath); err != nil {
|
||||||
// Mail datastore does not yet exist
|
// Mail datastore does not yet exist
|
||||||
os.MkdirAll(mailPath, 0770)
|
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
|
// DefaultFileDataStore creates a new DataStore object. It uses the inbucket.Config object to
|
||||||
// construct it's path.
|
// construct it's path.
|
||||||
func DefaultFileDataStore() DataStore {
|
func DefaultFileDataStore() DataStore {
|
||||||
path, err := config.Config.String("datastore", "path")
|
cfg := config.GetDataStoreConfig()
|
||||||
if err != nil {
|
return NewFileDataStore(cfg)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieves the Mailbox object for a specified email address, if the mailbox
|
// 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
|
// 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.
|
// 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()
|
date := time.Now()
|
||||||
id := generateId(date)
|
id := generateId(date)
|
||||||
|
return &FileMessage{mailbox: mb, Fid: id, Fdate: date, writable: true}, nil
|
||||||
return &FileMessage{mailbox: mb, Fid: id, Fdate: date, writable: true}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FileMessage) Id() string {
|
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
|
// 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) {
|
func (m *FileMessage) ReadHeader() (msg *mail.Message, err error) {
|
||||||
file, err := os.Open(m.rawPath())
|
file, err := os.Open(m.rawPath())
|
||||||
defer file.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer file.Close()
|
||||||
reader := bufio.NewReader(file)
|
reader := bufio.NewReader(file)
|
||||||
msg, err = mail.ReadMessage(reader)
|
msg, err = mail.ReadMessage(reader)
|
||||||
return msg, err
|
return msg, err
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package smtpd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/jhillyerd/inbucket/config"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -15,7 +16,7 @@ import (
|
|||||||
|
|
||||||
// Test directory structure created by filestore
|
// Test directory structure created by filestore
|
||||||
func TestFSDirStructure(t *testing.T) {
|
func TestFSDirStructure(t *testing.T) {
|
||||||
ds, logbuf := setupDataStore()
|
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||||
defer teardownDataStore(ds)
|
defer teardownDataStore(ds)
|
||||||
root := ds.path
|
root := ds.path
|
||||||
|
|
||||||
@@ -99,7 +100,7 @@ func TestFSDirStructure(t *testing.T) {
|
|||||||
|
|
||||||
// Test FileDataStore.AllMailboxes()
|
// Test FileDataStore.AllMailboxes()
|
||||||
func TestFSAllMailboxes(t *testing.T) {
|
func TestFSAllMailboxes(t *testing.T) {
|
||||||
ds, logbuf := setupDataStore()
|
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||||
defer teardownDataStore(ds)
|
defer teardownDataStore(ds)
|
||||||
|
|
||||||
for _, name := range []string{"abby", "bill", "christa", "donald", "evelyn"} {
|
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
|
// Test delivering several messages to the same mailbox, meanwhile querying its
|
||||||
// contents with a new mailbox object each time
|
// contents with a new mailbox object each time
|
||||||
func TestFSDeliverMany(t *testing.T) {
|
func TestFSDeliverMany(t *testing.T) {
|
||||||
ds, logbuf := setupDataStore()
|
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||||
defer teardownDataStore(ds)
|
defer teardownDataStore(ds)
|
||||||
|
|
||||||
mbName := "fred"
|
mbName := "fred"
|
||||||
@@ -176,7 +177,7 @@ func TestFSDeliverMany(t *testing.T) {
|
|||||||
|
|
||||||
// Test deleting messages
|
// Test deleting messages
|
||||||
func TestFSDelete(t *testing.T) {
|
func TestFSDelete(t *testing.T) {
|
||||||
ds, logbuf := setupDataStore()
|
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||||
defer teardownDataStore(ds)
|
defer teardownDataStore(ds)
|
||||||
|
|
||||||
mbName := "fred"
|
mbName := "fred"
|
||||||
@@ -250,7 +251,7 @@ func TestFSDelete(t *testing.T) {
|
|||||||
|
|
||||||
// Test purging a mailbox
|
// Test purging a mailbox
|
||||||
func TestFSPurge(t *testing.T) {
|
func TestFSPurge(t *testing.T) {
|
||||||
ds, logbuf := setupDataStore()
|
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||||
defer teardownDataStore(ds)
|
defer teardownDataStore(ds)
|
||||||
|
|
||||||
mbName := "fred"
|
mbName := "fred"
|
||||||
@@ -298,7 +299,7 @@ func TestFSPurge(t *testing.T) {
|
|||||||
|
|
||||||
// Test message size calculation
|
// Test message size calculation
|
||||||
func TestFSSize(t *testing.T) {
|
func TestFSSize(t *testing.T) {
|
||||||
ds, logbuf := setupDataStore()
|
ds, logbuf := setupDataStore(config.DataStoreConfig{})
|
||||||
defer teardownDataStore(ds)
|
defer teardownDataStore(ds)
|
||||||
|
|
||||||
mbName := "fred"
|
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
|
// 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")
|
path, err := ioutil.TempDir("", "inbucket")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -345,12 +468,14 @@ func setupDataStore() (*FileDataStore, *bytes.Buffer) {
|
|||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
log.SetOutput(buf)
|
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
|
// deliverMessage creates and delivers a message to the specific mailbox, returning
|
||||||
// the size of the generated message.
|
// 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
|
// Build fake SMTP message for delivery
|
||||||
testMsg := make([]byte, 0, 300)
|
testMsg := make([]byte, 0, 300)
|
||||||
testMsg = append(testMsg, []byte("To: somebody@host\r\n")...)
|
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
|
// Create message object
|
||||||
id = generateId(date)
|
id = generateId(date)
|
||||||
msg := &FileMessage{
|
msg, err := mb.NewMessage()
|
||||||
mailbox: mb.(*FileMailbox),
|
if err != nil {
|
||||||
writable: true,
|
panic(err)
|
||||||
Fdate: date,
|
|
||||||
Fid: id,
|
|
||||||
}
|
}
|
||||||
|
fmsg := msg.(*FileMessage)
|
||||||
|
fmsg.Fdate = date
|
||||||
|
fmsg.Fid = id
|
||||||
msg.Append(testMsg)
|
msg.Append(testMsg)
|
||||||
if err = msg.Close(); err != nil {
|
if err = msg.Close(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ const (
|
|||||||
QUIT // Close session
|
QUIT // Close session
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const STAMP_FMT = "Mon, 02 Jan 2006 15:04:05 -0700 (MST)"
|
||||||
|
|
||||||
func (s State) String() string {
|
func (s State) String() string {
|
||||||
switch s {
|
switch s {
|
||||||
case GREET:
|
case GREET:
|
||||||
@@ -62,6 +64,7 @@ type Session struct {
|
|||||||
server *Server
|
server *Server
|
||||||
id int
|
id int
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
|
remoteDomain string
|
||||||
remoteHost string
|
remoteHost string
|
||||||
sendError error
|
sendError error
|
||||||
state State
|
state State
|
||||||
@@ -196,9 +199,21 @@ func (s *Server) startSession(id int, conn net.Conn) {
|
|||||||
func (ss *Session) greetHandler(cmd string, arg string) {
|
func (ss *Session) greetHandler(cmd string, arg string) {
|
||||||
switch cmd {
|
switch cmd {
|
||||||
case "HELO":
|
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.send("250 Great, let's get this show on the road")
|
||||||
ss.enterState(READY)
|
ss.enterState(READY)
|
||||||
case "EHLO":
|
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-Great, let's get this show on the road")
|
||||||
ss.send("250-8BITMIME")
|
ss.send("250-8BITMIME")
|
||||||
ss.send(fmt.Sprintf("250 SIZE %v", ss.server.maxMessageBytes))
|
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
|
// READY state -> waiting for MAIL
|
||||||
func (ss *Session) readyHandler(cmd string, arg string) {
|
func (ss *Session) readyHandler(cmd string, arg string) {
|
||||||
if cmd == "MAIL" {
|
if cmd == "MAIL" {
|
||||||
@@ -305,11 +331,12 @@ func (ss *Session) mailHandler(cmd string, arg string) {
|
|||||||
|
|
||||||
// DATA
|
// DATA
|
||||||
func (ss *Session) dataHandler() {
|
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
|
// Get a Mailbox and a new Message for each recipient
|
||||||
mailboxes := make([]Mailbox, ss.recipients.Len())
|
mailboxes := make([]Mailbox, ss.recipients.Len())
|
||||||
messages := make([]Message, ss.recipients.Len())
|
messages := make([]Message, ss.recipients.Len())
|
||||||
|
msgSize := 0
|
||||||
if ss.server.storeMessages {
|
if ss.server.storeMessages {
|
||||||
i := 0
|
i := 0
|
||||||
for e := ss.recipients.Front(); e != nil; e = e.Next() {
|
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
|
// Not our "no store" domain, so store the message
|
||||||
mb, err := ss.server.dataStore.MailboxFor(local)
|
mb, err := ss.server.dataStore.MailboxFor(local)
|
||||||
if err != nil {
|
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.send(fmt.Sprintf("451 Failed to open mailbox for %v", local))
|
||||||
ss.reset()
|
ss.reset()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mailboxes[i] = mb
|
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 {
|
} else {
|
||||||
log.LogTrace("Not storing message for %q", recip)
|
log.LogTrace("Not storing message for %q", recip)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ func TestGreetState(t *testing.T) {
|
|||||||
|
|
||||||
// Test out some mangled HELOs
|
// Test out some mangled HELOs
|
||||||
script = []scriptStep{
|
script = []scriptStep{
|
||||||
|
{"HELO", 501},
|
||||||
|
{"EHLO", 501},
|
||||||
{"HELLO", 500},
|
{"HELLO", 500},
|
||||||
{"HELL", 500},
|
{"HELL", 500},
|
||||||
{"hello", 500},
|
{"hello", 500},
|
||||||
@@ -43,9 +45,6 @@ func TestGreetState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Valid HELOs
|
// 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 {
|
if err := playSession(t, server, []scriptStep{{"HELO mydomain", 250}}); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
@@ -55,6 +54,23 @@ func TestGreetState(t *testing.T) {
|
|||||||
if err := playSession(t, server, []scriptStep{{"HelO mydom.com", 250}}); err != nil {
|
if err := playSession(t, server, []scriptStep{{"HelO mydom.com", 250}}); err != nil {
|
||||||
t.Error(err)
|
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() {
|
if t.Failed() {
|
||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
@@ -136,7 +152,7 @@ func TestMailState(t *testing.T) {
|
|||||||
mb1 := &MockMailbox{}
|
mb1 := &MockMailbox{}
|
||||||
msg1 := &MockMessage{}
|
msg1 := &MockMessage{}
|
||||||
mds.On("MailboxFor").Return(mb1, nil)
|
mds.On("MailboxFor").Return(mb1, nil)
|
||||||
mb1.On("NewMessage").Return(msg1)
|
mb1.On("NewMessage").Return(msg1, nil)
|
||||||
msg1.On("Close").Return(nil)
|
msg1.On("Close").Return(nil)
|
||||||
|
|
||||||
server, logbuf := setupSmtpServer(mds)
|
server, logbuf := setupSmtpServer(mds)
|
||||||
@@ -246,7 +262,7 @@ func TestDataState(t *testing.T) {
|
|||||||
mb1 := &MockMailbox{}
|
mb1 := &MockMailbox{}
|
||||||
msg1 := &MockMessage{}
|
msg1 := &MockMessage{}
|
||||||
mds.On("MailboxFor").Return(mb1, nil)
|
mds.On("MailboxFor").Return(mb1, nil)
|
||||||
mb1.On("NewMessage").Return(msg1)
|
mb1.On("NewMessage").Return(msg1, nil)
|
||||||
msg1.On("Close").Return(nil)
|
msg1.On("Close").Return(nil)
|
||||||
|
|
||||||
server, logbuf := setupSmtpServer(mds)
|
server, logbuf := setupSmtpServer(mds)
|
||||||
@@ -317,7 +333,7 @@ func playScriptAgainst(t *testing.T, c *textproto.Conn, script []scriptStep) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.StartResponse(id)
|
c.StartResponse(id)
|
||||||
code, msg, err := c.ReadCodeLine(step.expect)
|
code, msg, err := c.ReadResponse(step.expect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("Step %d, sent %q, expected %v, got %v: %q",
|
err = fmt.Errorf("Step %d, sent %q, expected %v, got %v: %q",
|
||||||
i, step.send, step.expect, code, msg)
|
i, step.send, step.expect, code, msg)
|
||||||
|
|||||||
@@ -98,9 +98,9 @@ func (m *MockMailbox) Purge() error {
|
|||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMailbox) NewMessage() Message {
|
func (m *MockMailbox) NewMessage() (Message, error) {
|
||||||
args := m.Called()
|
args := m.Called()
|
||||||
return args.Get(0).(Message)
|
return args.Get(0).(Message), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMailbox) String() string {
|
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
|
swaks $* --h-Subject: "Swaks Plain Text" --body text.txt
|
||||||
|
|
||||||
# HTML test
|
# 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
|
# Attachment test
|
||||||
swaks $* --h-Subject: "Swaks Attachment" --attach-type image/png --attach favicon.png --body text.txt
|
swaks $* --h-Subject: "Swaks Attachment" --attach-type image/png --attach favicon.png --body text.txt
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ table.metrics {
|
|||||||
width: 15em;
|
width: 15em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metrics td {
|
.metrics td.number {
|
||||||
width: 10em;
|
width: 10em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ Released for free under a Creative Commons Attribution 2.5 License
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<p>Inbucket is an open source project hosted at
|
<p><a href="http://inbucket.org/">Inbucket</a> is an open source project hosted at
|
||||||
<a href="http://jhillyerd.github.io/inbucket/">github</a>.
|
<a href="http://github.com/jhillyerd/inbucket">github</a>.
|
||||||
Design by <a href="http://www.freecsstemplates.org/">FCT</a>.</p>
|
Design by <a href="http://www.freecsstemplates.org/">FCT</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -4,6 +4,14 @@
|
|||||||
{{define "script"}}
|
{{define "script"}}
|
||||||
<script>
|
<script>
|
||||||
var selected = "{{.selected}}"
|
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() {
|
function listLoaded() {
|
||||||
$('.listEntry').hover(
|
$('.listEntry').hover(
|
||||||
function() {
|
function() {
|
||||||
@@ -16,7 +24,7 @@
|
|||||||
function() {
|
function() {
|
||||||
$('.listEntry').removeClass("listEntrySelected")
|
$('.listEntry').removeClass("listEntrySelected")
|
||||||
$(this).addClass("listEntrySelected")
|
$(this).addClass("listEntrySelected")
|
||||||
$('#emailContent').load('/mailbox/{{.name}}/' + this.id)
|
$('#emailContent').load('/mailbox/{{.name}}/' + this.id, messageLoaded)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
$("#messageList").slideDown()
|
$("#messageList").slideDown()
|
||||||
|
|||||||
@@ -154,6 +154,10 @@
|
|||||||
<div class="box">
|
<div class="box">
|
||||||
<h3>Configuration</h3>
|
<h3>Configuration</h3>
|
||||||
<table class="metrics">
|
<table class="metrics">
|
||||||
|
<tr>
|
||||||
|
<th>Version:</th>
|
||||||
|
<td><span>{{.version}}, built on {{.buildDate}}</span></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>SMTP Listener:</th>
|
<th>SMTP Listener:</th>
|
||||||
<td><span>{{.smtpListener}}</span></td>
|
<td><span>{{.smtpListener}}</span></td>
|
||||||
@@ -174,29 +178,29 @@
|
|||||||
<table class="metrics">
|
<table class="metrics">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Uptime:</th>
|
<th>Uptime:</th>
|
||||||
<td><span id="m-uptime">.</span></td>
|
<td class="number"><span id="m-uptime">.</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>System Memory:</th>
|
<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 class="sparkline"><span id="s-memstatsSys">.</span></td>
|
||||||
<td>(10min)</td>
|
<td>(10min)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Heap Size:</th>
|
<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 class="sparkline"><span id="s-memstatsHeapSys">.</span></td>
|
||||||
<td>(10min)</td>
|
<td>(10min)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Heap In-Use:</th>
|
<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 class="sparkline"><span id="s-memstatsHeapAlloc">.</span></td>
|
||||||
<td>(10min)</td>
|
<td>(10min)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Heap # Objects:</th>
|
<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 class="sparkline"><span id="s-memstatsHeapObjects">.</span></td>
|
||||||
<td>(10min)</td>
|
<td>(10min)</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -208,31 +212,31 @@
|
|||||||
<table class="metrics">
|
<table class="metrics">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Current Connections:</th>
|
<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 class="sparkline"><span id="s-smtpConnectsCurrent">.</span></td>
|
||||||
<td>(10min)</td>
|
<td>(10min)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Total Connections:</th>
|
<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 class="sparkline"><span id="s-smtpConnectsTotal">.</span></td>
|
||||||
<td>(60min)</td>
|
<td>(60min)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Messages Received:</th>
|
<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 class="sparkline"><span id="s-smtpReceivedTotal">.</span></td>
|
||||||
<td>(60min)</td>
|
<td>(60min)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Errors Logged:</th>
|
<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 class="sparkline"><span id="s-smtpErrorsTotal"></span></td>
|
||||||
<td>(60min)</td>
|
<td>(60min)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Warnings Logged:</th>
|
<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 class="sparkline"><span id="s-smtpWarnsTotal"></span></td>
|
||||||
<td>(60min)</td>
|
<td>(60min)</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -264,13 +268,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Retention Deletes:</th>
|
<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 class="sparkline"><span id="s-retentionDeletesTotal"></span></td>
|
||||||
<td>(60min)</td>
|
<td>(60min)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Currently Retained:</th>
|
<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 class="sparkline"><span id="s-retainedCurrent"></span></td>
|
||||||
<td>(60min)</td>
|
<td>(60min)</td>
|
||||||
</tr>
|
</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)
|
return fmt.Errorf("MailboxFor('%v'): %v", name, err)
|
||||||
}
|
}
|
||||||
msg, err := mb.GetMessage(id)
|
msg, err := mb.GetMessage(id)
|
||||||
|
if err == smtpd.ErrNotExist {
|
||||||
|
http.NotFound(w, req)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("GetMessage() failed: %v", err)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
||||||
return RenderPartial("mailbox/_html.html", w, map[string]interface{}{
|
return RenderPartial("mailbox/_html.html", w, map[string]interface{}{
|
||||||
"ctx": ctx,
|
"ctx": ctx,
|
||||||
"name": name,
|
"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)
|
||||||
|
}
|
||||||
@@ -30,6 +30,8 @@ func RootStatus(w http.ResponseWriter, req *http.Request, ctx *Context) (err err
|
|||||||
config.GetWebConfig().Ip4port)
|
config.GetWebConfig().Ip4port)
|
||||||
return RenderTemplate("root/status.html", w, map[string]interface{}{
|
return RenderTemplate("root/status.html", w, map[string]interface{}{
|
||||||
"ctx": ctx,
|
"ctx": ctx,
|
||||||
|
"version": config.VERSION,
|
||||||
|
"buildDate": config.BUILD_DATE,
|
||||||
"retentionMinutes": retentionMinutes,
|
"retentionMinutes": retentionMinutes,
|
||||||
"smtpListener": smtpListener,
|
"smtpListener": smtpListener,
|
||||||
"pop3Listener": pop3Listener,
|
"pop3Listener": pop3Listener,
|
||||||
|
|||||||
@@ -18,12 +18,25 @@ import (
|
|||||||
|
|
||||||
type handler func(http.ResponseWriter, *http.Request, *Context) error
|
type handler func(http.ResponseWriter, *http.Request, *Context) error
|
||||||
|
|
||||||
|
var webConfig config.WebConfig
|
||||||
var DataStore smtpd.DataStore
|
var DataStore smtpd.DataStore
|
||||||
var Router *mux.Router
|
var Router *mux.Router
|
||||||
var listener net.Listener
|
var listener net.Listener
|
||||||
var sessionStore sessions.Store
|
var sessionStore sessions.Store
|
||||||
var shutdown bool
|
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) {
|
func setupRoutes(cfg config.WebConfig) {
|
||||||
log.LogInfo("Theme templates mapped to '%v'", cfg.TemplateDir)
|
log.LogInfo("Theme templates mapped to '%v'", cfg.TemplateDir)
|
||||||
log.LogInfo("Theme static content mapped to '%v'", cfg.PublicDir)
|
log.LogInfo("Theme static content mapped to '%v'", cfg.PublicDir)
|
||||||
@@ -54,16 +67,7 @@ func setupRoutes(cfg config.WebConfig) {
|
|||||||
|
|
||||||
// Start() the web server
|
// Start() the web server
|
||||||
func Start() {
|
func Start() {
|
||||||
cfg := config.GetWebConfig()
|
addr := fmt.Sprintf("%v:%v", webConfig.Ip4address, webConfig.Ip4port)
|
||||||
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)
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: nil,
|
Handler: nil,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jhillyerd/inbucket/config"
|
|
||||||
"github.com/jhillyerd/inbucket/log"
|
"github.com/jhillyerd/inbucket/log"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -49,9 +48,8 @@ func ParseTemplate(name string, partial bool) (*template.Template, error) {
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.GetWebConfig()
|
|
||||||
tempPath := strings.Replace(name, "/", string(filepath.Separator), -1)
|
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)
|
log.LogTrace("Parsing template %v", tempFile)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@@ -63,14 +61,14 @@ func ParseTemplate(name string, partial bool) (*template.Template, error) {
|
|||||||
t, err = t.ParseFiles(tempFile)
|
t, err = t.ParseFiles(tempFile)
|
||||||
} else {
|
} else {
|
||||||
t = template.New("_base.html").Funcs(TemplateFuncs)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allows us to disable caching for theme development
|
// Allows us to disable caching for theme development
|
||||||
if cfg.TemplateCache {
|
if webConfig.TemplateCache {
|
||||||
if partial {
|
if partial {
|
||||||
log.LogTrace("Caching partial %v", name)
|
log.LogTrace("Caching partial %v", name)
|
||||||
cachedTemplates[name] = t
|
cachedTemplates[name] = t
|
||||||
|
|||||||
Reference in New Issue
Block a user