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

Compare commits

...

11 Commits

Author SHA1 Message Date
James Hillyerd
13868d85d4 HTML view encoding fix
- HTML popup now specifies UTF8 encoding
- Version and build date are captured from goxc
- Version is displayed on status page, and initial log entry
2014-03-12 09:09:47 -07:00
James Hillyerd
8f10e18fef Fix a couple defered Close() operations 2014-01-20 14:32:58 -08:00
James Hillyerd
b105bbf87f Merge branch 'master' of https://github.com/jhillyerd/inbucket 2013-11-15 12:03:44 -08:00
James Hillyerd
ad85a1db93 Set content-type properly on MailboxHtml handler
- Changes to upstream go.enmime help with #20
- Setting content-type is required so that browsers correclty render
  docs that don't include an <html> element.
2013-11-15 12:03:27 -08:00
James Hillyerd
d418f4ba29 Update README and template for inbucket.org site 2013-11-14 20:28:29 -08:00
James Hillyerd
d98e6a2b58 Missing message is now 404 instead of 500
- Added smtpd.ErrNotExist to make detecting missing message easier
- Return 404 instead of 500 when requesting a non-extistant message
- More REST unit tests
2013-11-14 15:08:46 -08:00
James Hillyerd
8b7fbfda6a Unit test MailboxList JSON output 2013-11-13 15:22:03 -08:00
James Hillyerd
46fa714cc7 Add configurable mailbox message cap
- Add new configuration option [datastore]mailbox.message.cap
- Modify filestore to enforce message cap if value > 0
- Filestore unit tests for message cap when enabled & disabled
- Change to DataStore.Mailbox.NewMessage() interface to allow error
  return
2013-11-12 10:42:39 -08:00
James Hillyerd
414ed44882 Refactoring to support REST unit tests 2013-11-11 10:50:41 -08:00
James Hillyerd
2e1c937d23 Now adds a Recieved: header to raw message 2013-11-09 12:57:45 -08:00
James Hillyerd
df11575b3a Handle missing .raw better, closes #19
- Add filestore unit tests to exercise #19
- Move deferred file close under error check
- Handle error message from server gracefully on message click
- Scroll to top of page when message loads successfully
2013-11-09 08:47:20 -08:00
28 changed files with 914 additions and 123 deletions

View File

@@ -8,6 +8,6 @@
"Include": "README*,LICENSE*,inbucket.bat,etc,themes"
},
"PackageVersion": "1.0",
"PrereleaseInfo": "rc2",
"PrereleaseInfo": "rc4",
"FormatVersion": "0.8"
}

View File

@@ -2,34 +2,24 @@ Inbucket [![Build Status](https://travis-ci.org/jhillyerd/inbucket.png?branch=ma
========
Inbucket is an email testing service; it will accept messages for any email
address and make them available to view via a web interface.
address and make them available via web, REST and POP3. Once compiled,
Inbucket does not have an external dependencies (HTTP, SMTP, POP3 and storage
are all built in).
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/

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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() {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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 {

View 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.

View File

@@ -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

View File

@@ -320,7 +320,7 @@ table.metrics {
width: 15em;
}
.metrics td {
.metrics td.number {
width: 10em;
}

View File

@@ -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>

View File

@@ -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()

View File

@@ -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>

View File

@@ -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
View 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)
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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